From f93949583aebadcc31a9fdd9a3acc299530d0f4e Mon Sep 17 00:00:00 2001 From: ludacirs Date: Mon, 11 Mar 2024 20:13:27 +0900 Subject: [PATCH 1/3] fix: Add a unit to the value where the unit is omitted --- src/__tests__/to-have-style.js | 11 ++++ src/to-have-style.js | 92 ++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/__tests__/to-have-style.js b/src/__tests__/to-have-style.js index 5991a7e..bd97ae9 100644 --- a/src/__tests__/to-have-style.js +++ b/src/__tests__/to-have-style.js @@ -215,6 +215,17 @@ describe('.toHaveStyle', () => { }) }) + test('Fails when unit is omitted and the style does not match', () => { + const {queryByTestId} = render(` + Hello World + `) + expect(() => { + expect(queryByTestId('color-example')).toHaveStyle({ + fontSize: 8, + }) + }).toThrow() + }) + test('Fails with an invalid unit', () => { const {queryByTestId} = render(` Hello World diff --git a/src/to-have-style.js b/src/to-have-style.js index 010e2a5..387454f 100644 --- a/src/to-have-style.js +++ b/src/to-have-style.js @@ -1,16 +1,100 @@ import chalk from 'chalk' import {checkHtmlElement, parseCSS} from './utils' +/** https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/shared/isUnitlessNumber.js */ +const unitlessNumbers = new Set([ + 'animationIterationCount', + 'aspectRatio', + 'borderImageOutset', + 'borderImageSlice', + 'borderImageWidth', + 'boxFlex', + 'boxFlexGroup', + 'boxOrdinalGroup', + 'columnCount', + 'columns', + 'flex', + 'flexGrow', + 'flexPositive', + 'flexShrink', + 'flexNegative', + 'flexOrder', + 'gridArea', + 'gridRow', + 'gridRowEnd', + 'gridRowSpan', + 'gridRowStart', + 'gridColumn', + 'gridColumnEnd', + 'gridColumnSpan', + 'gridColumnStart', + 'fontWeight', + 'lineClamp', + 'lineHeight', + 'opacity', + 'order', + 'orphans', + 'scale', + 'tabSize', + 'widows', + 'zIndex', + 'zoom', + 'fillOpacity', // SVG-related properties + 'floodOpacity', + 'stopOpacity', + 'strokeDasharray', + 'strokeDashoffset', + 'strokeMiterlimit', + 'strokeOpacity', + 'strokeWidth', + 'MozAnimationIterationCount', // Known Prefixed Properties + 'MozBoxFlex', // TODO: Remove these since they shouldn't be used in modern code + 'MozBoxFlexGroup', + 'MozLineClamp', + 'msAnimationIterationCount', + 'msFlex', + 'msZoom', + 'msFlexGrow', + 'msFlexNegative', + 'msFlexOrder', + 'msFlexPositive', + 'msFlexShrink', + 'msGridColumn', + 'msGridColumnSpan', + 'msGridRow', + 'msGridRowSpan', + 'WebkitAnimationIterationCount', + 'WebkitBoxFlex', + 'WebKitBoxFlexGroup', + 'WebkitBoxOrdinalGroup', + 'WebkitColumnCount', + 'WebkitColumns', + 'WebkitFlex', + 'WebkitFlexGrow', + 'WebkitFlexPositive', + 'WebkitFlexShrink', + 'WebkitLineClamp', +]) + +function isUnitProperty([property, value]) { + if (typeof value !== 'number') { + return false + } + + return !unitlessNumbers.has(property) +} + function getStyleDeclaration(document, css) { const styles = {} // The next block is necessary to normalize colors const copy = document.createElement('div') - Object.keys(css).forEach(property => { - copy.style[property] = css[property] - styles[property] = copy.style[property] - }) + Object.entries(css).forEach(entry => { + const [prop, value] = entry + copy.style[prop] = isUnitProperty(entry) ? `${value}px` : value + styles[prop] = copy.style[prop] + }) return styles } From b6e86a7d100126ff80b6419042fd125959539b02 Mon Sep 17 00:00:00 2001 From: ludacirs Date: Mon, 11 Mar 2024 20:14:44 +0900 Subject: [PATCH 2/3] fix: Change property from camelCase to snake-case --- src/__tests__/to-have-style.js | 26 ++++++++++++++++++++++++++ src/to-have-style.js | 23 ++++++++++++++++------- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/__tests__/to-have-style.js b/src/__tests__/to-have-style.js index bd97ae9..a8f2fd9 100644 --- a/src/__tests__/to-have-style.js +++ b/src/__tests__/to-have-style.js @@ -262,4 +262,30 @@ describe('.toHaveStyle', () => { }) }) }) + + describe('Fails when invalid value of property', () => { + test('with empty strings', () => { + const {container} = render(` +
+ `) + + expect(() => + expect(container.querySelector('.border')).toHaveStyle({ + borderWidth: '', + }), + ).toThrow() + }) + + test('with strings without unit', () => { + const {container} = render(` +
+ `) + + expect(() => { + expect(container.querySelector('.border')).toHaveStyle({ + borderWidth: '2', + }) + }).toThrow() + }) + }) }) diff --git a/src/to-have-style.js b/src/to-have-style.js index 387454f..672e567 100644 --- a/src/to-have-style.js +++ b/src/to-have-style.js @@ -76,6 +76,14 @@ const unitlessNumbers = new Set([ 'WebkitLineClamp', ]) +function camelToKebab(camelCaseString) { + return camelCaseString.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() +} + +function isCustomProperty(property) { + return property.startsWith('--') +} + function isUnitProperty([property, value]) { if (typeof value !== 'number') { return false @@ -90,7 +98,8 @@ function getStyleDeclaration(document, css) { // The next block is necessary to normalize colors const copy = document.createElement('div') Object.entries(css).forEach(entry => { - const [prop, value] = entry + const [property, value] = entry + const prop = isCustomProperty(property) ? property : camelToKebab(property) copy.style[prop] = isUnitProperty(entry) ? `${value}px` : value styles[prop] = copy.style[prop] @@ -102,15 +111,15 @@ function isSubset(styles, computedStyle) { return ( !!Object.keys(styles).length && Object.entries(styles).every(([prop, value]) => { - const isCustomProperty = prop.startsWith('--') const spellingVariants = [prop] - if (!isCustomProperty) spellingVariants.push(prop.toLowerCase()) + if (!isCustomProperty(prop)) spellingVariants.push(prop.toLowerCase()) - return spellingVariants.some( - name => + return spellingVariants.some(name => { + return ( computedStyle[name] === value || - computedStyle.getPropertyValue(name) === value, - ) + computedStyle.getPropertyValue(name) === value + ) + }) }) ) } From 6493f1fc4d82d9207a9d39fad46df000453f7dbf Mon Sep 17 00:00:00 2001 From: ludacirs Date: Thu, 14 Mar 2024 23:59:58 +0900 Subject: [PATCH 3/3] fix: Extract convertCssObject and relocate camelToKeba --- src/to-have-style.js | 46 ++++++++++++++++++++++++++++++-------------- src/utils.js | 4 ++++ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/to-have-style.js b/src/to-have-style.js index 672e567..f53a66b 100644 --- a/src/to-have-style.js +++ b/src/to-have-style.js @@ -1,7 +1,14 @@ import chalk from 'chalk' -import {checkHtmlElement, parseCSS} from './utils' +import {checkHtmlElement, parseCSS, camelToKebab} from './utils' -/** https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/shared/isUnitlessNumber.js */ +/** + * Set of CSS properties that typically have unitless values. + * These properties are commonly used in CSS without specifying units such as px, em, etc. + * This set is used to determine whether a numerical value should have a unit appended to it. + * + * Note: This list is based on the `isUnitlessNumber` module from the React DOM package. + * Source: https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/shared/isUnitlessNumber.js + */ const unitlessNumbers = new Set([ 'animationIterationCount', 'aspectRatio', @@ -76,15 +83,11 @@ const unitlessNumbers = new Set([ 'WebkitLineClamp', ]) -function camelToKebab(camelCaseString) { - return camelCaseString.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() -} - function isCustomProperty(property) { return property.startsWith('--') } -function isUnitProperty([property, value]) { +function isUnitProperty(property, value) { if (typeof value !== 'number') { return false } @@ -92,17 +95,31 @@ function isUnitProperty([property, value]) { return !unitlessNumbers.has(property) } +/** + * Convert a CSS object to a valid style object. + * This function takes a CSS object and converts it into a valid style object. + * It transforms camelCase property names to kebab-case and appends 'px' unit to numerical values. + */ +function convertCssObject(css) { + return Object.entries(css).reduce((obj, [property, value]) => { + const styleProperty = isCustomProperty(property) + ? property + : camelToKebab(property) + const styleValue = isUnitProperty(property, value) ? `${value}px` : value + return Object.assign(obj, { + [styleProperty]: styleValue, + }) + }, {}) +} + function getStyleDeclaration(document, css) { const styles = {} // The next block is necessary to normalize colors const copy = document.createElement('div') - Object.entries(css).forEach(entry => { - const [property, value] = entry - const prop = isCustomProperty(property) ? property : camelToKebab(property) - - copy.style[prop] = isUnitProperty(entry) ? `${value}px` : value - styles[prop] = copy.style[prop] + Object.entries(css).forEach(([property, value]) => { + copy.style[property] = value + styles[property] = copy.style[property] }) return styles } @@ -150,9 +167,10 @@ export function toHaveStyle(htmlElement, css) { checkHtmlElement(htmlElement, toHaveStyle, this) const parsedCSS = typeof css === 'object' ? css : parseCSS(css, toHaveStyle, this) + const cssObject = convertCssObject(parsedCSS) const {getComputedStyle} = htmlElement.ownerDocument.defaultView - const expected = getStyleDeclaration(htmlElement.ownerDocument, parsedCSS) + const expected = getStyleDeclaration(htmlElement.ownerDocument, cssObject) const received = getComputedStyle(htmlElement) return { diff --git a/src/utils.js b/src/utils.js index 903f24c..1d16295 100644 --- a/src/utils.js +++ b/src/utils.js @@ -227,6 +227,9 @@ function toSentence( array.length > 1 ? lastWordConnector : '', ) } +function camelToKebab(camelCaseString) { + return camelCaseString.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() +} export { HtmlElementTypeError, @@ -242,4 +245,5 @@ export { getSingleElementValue, compareArraysAsSet, toSentence, + camelToKebab, }