|
| 1 | +import {parse} from 'css' |
| 2 | +import {matcherHint} from 'jest-matcher-utils' |
| 3 | +import jestDiff from 'jest-diff' |
| 4 | +import chalk from 'chalk' |
| 5 | +import {checkHtmlElement} from './utils' |
| 6 | + |
| 7 | +function parseCSS(css) { |
| 8 | + const ast = parse(`selector { ${css} }`, {silent: true}).stylesheet |
| 9 | + if (ast.parsingErrors && ast.parsingErrors.length > 0) { |
| 10 | + const {reason, line, column} = ast.parsingErrors[0] |
| 11 | + return { |
| 12 | + parsingError: `Syntax error parsing expected css: ${reason} in ${line}:${column}`, |
| 13 | + } |
| 14 | + } |
| 15 | + const parsedRules = ast.rules[0].declarations |
| 16 | + .filter(d => d.type === 'declaration') |
| 17 | + .reduce( |
| 18 | + (obj, {property, value}) => Object.assign(obj, {[property]: value}), |
| 19 | + {}, |
| 20 | + ) |
| 21 | + return {parsedRules} |
| 22 | +} |
| 23 | + |
| 24 | +function isSubset(styles, computedStyle) { |
| 25 | + return Object.entries(styles).every( |
| 26 | + ([prop, value]) => computedStyle.getPropertyValue(prop) === value, |
| 27 | + ) |
| 28 | +} |
| 29 | + |
| 30 | +function printoutStyles(styles) { |
| 31 | + return Object.keys(styles) |
| 32 | + .sort() |
| 33 | + .map(prop => `${prop}: ${styles[prop]};`) |
| 34 | + .join('\n') |
| 35 | +} |
| 36 | + |
| 37 | +// Highlights only style rules that were expected but were not found in the |
| 38 | +// received computed styles |
| 39 | +function expectedDiff(expected, computedStyles) { |
| 40 | + const received = Array.from(computedStyles) |
| 41 | + .filter(prop => expected[prop]) |
| 42 | + .reduce( |
| 43 | + (obj, prop) => |
| 44 | + Object.assign(obj, {[prop]: computedStyles.getPropertyValue(prop)}), |
| 45 | + {}, |
| 46 | + ) |
| 47 | + const diffOutput = jestDiff( |
| 48 | + printoutStyles(expected), |
| 49 | + printoutStyles(received), |
| 50 | + ) |
| 51 | + // Remove the "+ Received" annotation because this is a one-way diff |
| 52 | + return diffOutput.replace(`${chalk.red('+ Received')}\n`, '') |
| 53 | +} |
| 54 | + |
| 55 | +export function toHaveStyle(htmlElement, css) { |
| 56 | + checkHtmlElement(htmlElement) |
| 57 | + const {parsedRules: expected, parsingError} = parseCSS(css) |
| 58 | + if (parsingError) { |
| 59 | + return { |
| 60 | + pass: this.isNot, // Fail regardless of the test being positive or negative |
| 61 | + message: () => parsingError, |
| 62 | + } |
| 63 | + } |
| 64 | + const received = getComputedStyle(htmlElement) |
| 65 | + return { |
| 66 | + pass: isSubset(expected, received), |
| 67 | + message: () => { |
| 68 | + const matcher = `${this.isNot ? '.not' : ''}.toHaveStyle` |
| 69 | + return [ |
| 70 | + matcherHint(matcher, 'element', ''), |
| 71 | + expectedDiff(expected, received), |
| 72 | + ].join('\n\n') |
| 73 | + }, |
| 74 | + } |
| 75 | +} |
0 commit comments