diff --git a/src/__tests__/to-have-style.js b/src/__tests__/to-have-style.js index 5c930e10..b66b9bc2 100644 --- a/src/__tests__/to-have-style.js +++ b/src/__tests__/to-have-style.js @@ -1,59 +1,98 @@ import {render} from './helpers/test-utils' -test('.toHaveStyle', () => { - const {container} = render(` +describe('.toHaveStyle', () => { + test('handles positive test cases', () => { + const {container} = render(` +
+ Hello World +
+ `) + + const style = document.createElement('style') + style.innerHTML = ` + .label { + background-color: black; + color: white; + float: left; + } + ` + document.body.appendChild(style) + document.body.appendChild(container) + + expect(container.querySelector('.label')).toHaveStyle(` + height: 100%; + color: white; + background-color: blue; + `) + + expect(container.querySelector('.label')).toHaveStyle(` + background-color: blue; + color: white; + `) + expect(container.querySelector('.label')).toHaveStyle( + 'background-color:blue;color:white', + ) + + expect(container.querySelector('.label')).not.toHaveStyle(` + color: white; + font-weight: bold; + `) + }) + + test('handles negative test cases', () => { + const {container} = render(`
Hello World
`) - const style = document.createElement('style') - style.innerHTML = ` + const style = document.createElement('style') + style.innerHTML = ` .label { background-color: black; color: white; float: left; } ` - document.body.appendChild(style) - document.body.appendChild(container) + document.body.appendChild(style) + document.body.appendChild(container) - expect(container.querySelector('.label')).toHaveStyle(` - height: 100%; - color: white; - background-color: blue; - `) + expect(() => + expect(container.querySelector('.label')).toHaveStyle( + 'font-weight: bold', + ), + ).toThrowError() + expect(() => + expect(container.querySelector('.label')).not.toHaveStyle('color: white'), + ).toThrowError() - expect(container.querySelector('.label')).toHaveStyle(` - background-color: blue; - color: white; - `) - expect(container.querySelector('.label')).toHaveStyle( - 'background-color:blue;color:white', - ) + // Make sure the test fails if the css syntax is not valid + expect(() => + expect(container.querySelector('.label')).not.toHaveStyle( + 'font-weight bold', + ), + ).toThrowError() + expect(() => + expect(container.querySelector('.label')).toHaveStyle('color white'), + ).toThrowError() - expect(container.querySelector('.label')).not.toHaveStyle(` - color: white; - font-weight: bold; - `) + document.body.removeChild(style) + document.body.removeChild(container) + }) - expect(() => - expect(container.querySelector('.label')).toHaveStyle('font-weight: bold'), - ).toThrowError() - expect(() => - expect(container.querySelector('.label')).not.toHaveStyle('color: white'), - ).toThrowError() - - // Make sure the test fails if the css syntax is not valid - expect(() => - expect(container.querySelector('.label')).not.toHaveStyle( - 'font-weight bold', - ), - ).toThrowError() - expect(() => - expect(container.querySelector('.label')).toHaveStyle('color white'), - ).toThrowError() - - document.body.removeChild(style) - document.body.removeChild(container) + test('properly normalizes colors', () => { + const {queryByTestId} = render(` + Hello World + `) + expect(queryByTestId('color-example')).toHaveStyle( + 'background-color: #123456', + ) + }) + + test('properly normalizes colors for border', () => { + const {queryByTestId} = render(` + Hello World + `) + expect(queryByTestId('color-example')).toHaveStyle('border: 1px solid #fff') + }) }) diff --git a/src/to-have-style.js b/src/to-have-style.js index 70670a10..2f9493b1 100644 --- a/src/to-have-style.js +++ b/src/to-have-style.js @@ -1,24 +1,17 @@ -import {parse} from 'css' import {matcherHint} from 'jest-matcher-utils' import jestDiff from 'jest-diff' import chalk from 'chalk' -import {checkHtmlElement} from './utils' +import {checkHtmlElement, checkValidCSS} from './utils' -function parseCSS(css) { - const ast = parse(`selector { ${css} }`, {silent: true}).stylesheet - if (ast.parsingErrors && ast.parsingErrors.length > 0) { - const {reason, line, column} = ast.parsingErrors[0] - return { - parsingError: `Syntax error parsing expected css: ${reason} in ${line}:${column}`, - } - } - const parsedRules = ast.rules[0].declarations - .filter(d => d.type === 'declaration') - .reduce( - (obj, {property, value}) => Object.assign(obj, {[property]: value}), - {}, - ) - return {parsedRules} +function getStyleDeclaration(css) { + const copy = document.createElement('div') + copy.style = css + const styles = copy.style + + return Array.from(styles).reduce( + (acc, name) => ({...acc, [name]: styles[name]}), + {}, + ) } function isSubset(styles, computedStyle) { @@ -55,14 +48,11 @@ function expectedDiff(expected, computedStyles) { export function toHaveStyle(htmlElement, css) { checkHtmlElement(htmlElement, toHaveStyle, this) - const {parsedRules: expected, parsingError} = parseCSS(css) - if (parsingError) { - return { - pass: this.isNot, // Fail regardless of the test being positive or negative - message: () => parsingError, - } - } + checkValidCSS(css, toHaveStyle, this) + + const expected = getStyleDeclaration(css) const received = getComputedStyle(htmlElement) + return { pass: isSubset(expected, received), message: () => { diff --git a/src/utils.js b/src/utils.js index 2bd89492..d83ca385 100644 --- a/src/utils.js +++ b/src/utils.js @@ -7,6 +7,7 @@ import { printReceived, stringify, } from 'jest-matcher-utils' +import {parse} from 'css' class HtmlElementTypeError extends Error { constructor(received, matcherFn, context) { @@ -40,6 +41,39 @@ function checkHtmlElement(htmlElement, ...args) { } } +class InvalidCSSError extends Error { + constructor(received, matcherFn) { + super() + + /* istanbul ignore next */ + if (Error.captureStackTrace) { + Error.captureStackTrace(this, matcherFn) + } + this.message = [ + received.message, + '', + receivedColor(`Failing css:`), + receivedColor(`${received.css}`), + ].join('\n') + } +} + +function checkValidCSS(css, ...args) { + const ast = parse(`selector { ${css} }`, {silent: true}).stylesheet + + if (ast.parsingErrors && ast.parsingErrors.length > 0) { + const {reason, line} = ast.parsingErrors[0] + + throw new InvalidCSSError( + { + css, + message: `Syntax error parsing expected css: ${reason} on line: ${line}`, + }, + ...args, + ) + } +} + class InvalidDocumentError extends Error { constructor(message, matcherFn) { super() @@ -108,4 +142,11 @@ function deprecate(name, replacementText) { ) } -export {checkDocumentKey, checkHtmlElement, deprecate, getMessage, matches} +export { + checkDocumentKey, + checkHtmlElement, + checkValidCSS, + deprecate, + getMessage, + matches, +}