Skip to content

Commit

Permalink
Improved reliability of snapshots by having consistent order of extra…
Browse files Browse the repository at this point in the history
…cted rules (#1880)

* Fix inconsistent style ordering for snapshots

Due to jest writing styles in the order it encounters
rendered styled components across tests, an issue can
occur where skipping/removing/reordering your tests will
invalidate test snapshots.

This fix sorts the style elements when serializing emotion styles.

* Add changeset for @jest/emotion style sorting

* update snapshots for style reordering fix

* Rewrite style extraction in the serializer to be independent of the rules insertion order

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
Jimmydalecleveland and Andarist committed Aug 9, 2020
1 parent e07873b commit 8a88e77
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 130 deletions.
5 changes: 5 additions & 0 deletions .changeset/fresh-clouds-tell.md
@@ -0,0 +1,5 @@
---
'@emotion/jest': patch
---

Improved stability of the generated snapshots - styles are extracted now based on the order in which the associated with them class names appear in the serialized elements rather than based on the order of the actual rules in the document.
12 changes: 6 additions & 6 deletions packages/css/test/__snapshots__/component-selector.test.js.snap
@@ -1,19 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`component selector should be converted to use the emotion target className 1`] = `
.emotion-0 {
color: blue;
.emotion-0 .emotion-2 {
color: red;
}
.emotion-2 .emotion-1 {
color: red;
.emotion-1 {
color: blue;
}
<div
className="emotion-2"
className="emotion-0"
>
<div
className="emotion-0 emotion-1"
className="emotion-1 emotion-2"
/>
</div>
`;
32 changes: 0 additions & 32 deletions packages/css/test/__snapshots__/css.test.js.snap
Expand Up @@ -439,38 +439,6 @@ exports[`css nested at rule 1`] = `
/>
`;

exports[`css nested at rules 1`] = `
@media (min-width: 420px) {
.glamor-0 {
color: pink;
}
}
@media (min-width: 420px) and (max-width: 500px) {
.glamor-0 {
color: hotpink;
}
}
@supports (display: grid) {
.glamor-0 {
display: grid;
}
}
@supports (display: grid) and ((display: -webkit-box) or (display: flex)) {
.glamor-0 {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
}
<div
className="glamor-0"
/>
`;

exports[`css null rule 1`] = `
<div
className="emotion-0"
Expand Down
8 changes: 4 additions & 4 deletions packages/jest/src/create-serializer.js
Expand Up @@ -23,16 +23,16 @@ function getNodes(node, nodes = []) {
return nodes
}

if (typeof node === 'object') {
nodes.push(node)
}

if (node.children) {
for (let child of node.children) {
getNodes(child, nodes)
}
}

if (typeof node === 'object') {
nodes.push(node)
}

return nodes
}

Expand Down
72 changes: 55 additions & 17 deletions packages/jest/src/utils.js
Expand Up @@ -18,6 +18,20 @@ export function findLast<T>(arr: T[], predicate: T => boolean) {
}
}

export function findIndexFrom<T>(
arr: T[],
fromIndex: number,
predicate: T => boolean
) {
for (let i = fromIndex; i < arr.length; i++) {
if (predicate(arr[i])) {
return i
}
}

return -1
}

function getClassNames(selectors: any, classes?: string) {
return classes ? selectors.concat(classes.split(' ')) : selectors
}
Expand Down Expand Up @@ -128,6 +142,19 @@ const getElementRules = (element: HTMLStyleElement): string[] => {
return [].slice.call(element.sheet.cssRules).map(cssRule => cssRule.cssText)
}

const getKeyframesMap = rules =>
rules.reduce((keyframes, rule) => {
const match = rule.match(keyframesPattern)
if (match !== null) {
const name = match[1]
if (keyframes[name] === undefined) {
keyframes[name] = ''
}
keyframes[name] += rule
}
return keyframes
}, {})

export function getStylesFromClassNames(
classNames: Array<string>,
elements: Array<HTMLStyleElement>
Expand Down Expand Up @@ -155,25 +182,36 @@ export function getStylesFromClassNames(
return ''
}
const selectorPattern = new RegExp(
'\\.(' + filteredClassNames.join('|') + ')'
'\\.(?:' + filteredClassNames.map(cls => `(${cls})`).join('|') + ')'
)
const keyframes = {}
let styles = ''

flatMap(elements, getElementRules).forEach((rule: string) => {
if (selectorPattern.test(rule)) {
styles += rule
}
const match = rule.match(keyframesPattern)
if (match !== null) {
const name = match[1]
if (keyframes[name] === undefined) {
keyframes[name] = ''
const rules = flatMap(elements, getElementRules)

let styles = rules
.map((rule: string) => {
const match = rule.match(selectorPattern)
if (!match) {
return null
}
keyframes[name] += rule
}
})
const keyframeNameKeys = Object.keys(keyframes)
// `selectorPattern` represents all emotion-generated class names
// each possible class name is wrapped in a capturing group
// and those groups appear in the same order as they appear in the DOM within class attributes
// because we've gathered them from the DOM in such order
// given that information we can sort matched rules based on the capturing group that has been matched
// to end up with styles in a stable order
const matchedCapturingGroupIndex = findIndexFrom(match, 1, Boolean)
return [rule, matchedCapturingGroupIndex]
})
.filter(Boolean)
.sort(
([ruleA, classNameIndexA], [ruleB, classNameIndexB]) =>
classNameIndexA - classNameIndexB
)
.map(([rule]) => rule)
.join('')

const keyframesMap = getKeyframesMap(rules)
const keyframeNameKeys = Object.keys(keyframesMap)
let keyframesStyles = ''

if (keyframeNameKeys.length) {
Expand All @@ -184,7 +222,7 @@ export function getStylesFromClassNames(
styles = styles.replace(keyframesNamePattern, name => {
if (keyframesNameCache[name] === undefined) {
keyframesNameCache[name] = `animation-${index++}`
keyframesStyles += keyframes[name]
keyframesStyles += keyframesMap[name]
}
return keyframesNameCache[name]
})
Expand Down
8 changes: 4 additions & 4 deletions packages/jest/test/__snapshots__/preact.test.js.snap
Expand Up @@ -3,19 +3,19 @@
exports[`jest-emotion with preact handles elements with no props 1`] = `"<div />"`;

exports[`jest-emotion with preact replaces class names and inserts styles into preact test component snapshots 1`] = `
".emotion-1 {
".emotion-0 {
color: red;
}
.emotion-0 {
.emotion-1 {
width: 100%;
}
<div
class=\\"emotion-1\\"
class=\\"emotion-0\\"
>
<svg
class=\\"emotion-0\\"
class=\\"emotion-1\\"
/>
</div>"
`;
24 changes: 12 additions & 12 deletions packages/jest/test/__snapshots__/printer.test.js.snap
Expand Up @@ -21,55 +21,55 @@ exports[`jest-emotion with DOM elements disabled does not replace class names or
`;

exports[`jest-emotion with DOM elements disabled replaces class names and inserts styles into React test component snapshots 1`] = `
".emotion-1 {
".emotion-0 {
color: red;
}
.emotion-0 {
.emotion-1 {
width: 100%;
}
<div
className=\\"emotion-1\\"
className=\\"emotion-0\\"
>
<svg
className=\\"emotion-0\\"
className=\\"emotion-1\\"
/>
</div>"
`;

exports[`jest-emotion with dom elements replaces class names and inserts styles into DOM element snapshots 1`] = `
".emotion-1 {
".emotion-0 {
color: red;
}
.emotion-0 {
.emotion-1 {
width: 100%;
}
<div
class=\\"emotion-1\\"
class=\\"emotion-0\\"
>
<svg
class=\\"emotion-0\\"
class=\\"emotion-1\\"
/>
</div>"
`;

exports[`jest-emotion with dom elements replaces class names and inserts styles into React test component snapshots 1`] = `
".emotion-1 {
".emotion-0 {
color: red;
}
.emotion-0 {
.emotion-1 {
width: 100%;
}
<div
className=\\"emotion-1\\"
className=\\"emotion-0\\"
>
<svg
className=\\"emotion-0\\"
className=\\"emotion-1\\"
/>
</div>"
`;
Expand Down
12 changes: 6 additions & 6 deletions packages/react/__tests__/__snapshots__/use-theme.js.snap
@@ -1,28 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Nested useTheme works 1`] = `
.emotion-1 {
.emotion-0 {
color: green;
}
.emotion-1:hover {
.emotion-0:hover {
color: darkgreen;
}
.emotion-0 {
.emotion-1 {
color: lawngreen;
}
.emotion-0:hover {
.emotion-1:hover {
color: seagreen;
}
<div
className="emotion-1"
className="emotion-0"
>
Should be green
<div
className="emotion-0"
className="emotion-1"
>
Should be lawngreen
</div>
Expand Down
30 changes: 15 additions & 15 deletions packages/styled/__tests__/__snapshots__/styled.js.snap
Expand Up @@ -45,22 +45,22 @@ exports[`styled call expression 1`] = `

exports[`styled component selectors 1`] = `
.emotion-0 {
color: hotpink;
}
.emotion-2 {
color: green;
}
.emotion-2 .emotion-1 {
.emotion-0 .emotion-2 {
color: yellow;
}
.emotion-1 {
color: hotpink;
}
<div
className="emotion-2"
className="emotion-0"
>
<div
className="emotion-0 emotion-1"
className="emotion-1 emotion-2"
/>
</div>
`;
Expand Down Expand Up @@ -329,30 +329,30 @@ exports[`styled keyframes with css call 1`] = `

exports[`styled nested 1`] = `
.emotion-0 {
font-size: 20px;
}
.emotion-1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.emotion-1 div {
.emotion-0 div {
color: green;
}
.emotion-1 div span {
.emotion-0 div span {
color: red;
}
.emotion-1 {
font-size: 20px;
}
<div
className="emotion-1"
className="emotion-0"
>
hello
<h1
className="emotion-0"
className="emotion-1"
>
This will be green
</h1>
Expand Down

0 comments on commit 8a88e77

Please sign in to comment.