Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add a convertCssObject to create a valid style object within the toHaveStyle. #591

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/__tests__/to-have-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,17 @@ describe('.toHaveStyle', () => {
})
})

test('Fails when unit is omitted and the style does not match', () => {
const {queryByTestId} = render(`
<span data-testid="color-example" style="font-size: 12px">Hello World</span>
`)
expect(() => {
expect(queryByTestId('color-example')).toHaveStyle({
fontSize: 8,
})
}).toThrow()
})

test('Fails with an invalid unit', () => {
const {queryByTestId} = render(`
<span data-testid="color-example" style="font-size: 12rem">Hello World</span>
Expand Down Expand Up @@ -251,4 +262,30 @@ describe('.toHaveStyle', () => {
})
})
})

describe('Fails when invalid value of property', () => {
test('with empty strings', () => {
const {container} = render(`
<div class="border" style="border-width: 2px;" />
`)

expect(() =>
expect(container.querySelector('.border')).toHaveStyle({
borderWidth: '',
}),
).toThrow()
})

test('with strings without unit', () => {
const {container} = render(`
<div class="border" style="border-width: 2px" />
`)

expect(() => {
expect(container.querySelector('.border')).toHaveStyle({
borderWidth: '2',
})
}).toThrow()
})
})
})
133 changes: 122 additions & 11 deletions src/to-have-style.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,142 @@
import chalk from 'chalk'
import {checkHtmlElement, parseCSS} from './utils'
import {checkHtmlElement, parseCSS, camelToKebab} from './utils'

/**
* 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',
'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 isCustomProperty(property) {
return property.startsWith('--')
}

function isUnitProperty(property, value) {
if (typeof value !== 'number') {
return false
}

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.keys(css).forEach(property => {
copy.style[property] = css[property]
Object.entries(css).forEach(([property, value]) => {
copy.style[property] = value
styles[property] = copy.style[property]
})

return styles
}

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
)
})
})
)
}
Expand Down Expand Up @@ -57,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 {
Expand Down
4 changes: 4 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -242,4 +245,5 @@ export {
getSingleElementValue,
compareArraysAsSet,
toSentence,
camelToKebab,
}