Skip to content

Commit 8a88e77

Browse files
Improved reliability of snapshots by having consistent order of extracted 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 <[email protected]>
1 parent e07873b commit 8a88e77

File tree

11 files changed

+141
-130
lines changed

11 files changed

+141
-130
lines changed

.changeset/fresh-clouds-tell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@emotion/jest': patch
3+
---
4+
5+
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.
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`component selector should be converted to use the emotion target className 1`] = `
4-
.emotion-0 {
5-
color: blue;
4+
.emotion-0 .emotion-2 {
5+
color: red;
66
}
77
8-
.emotion-2 .emotion-1 {
9-
color: red;
8+
.emotion-1 {
9+
color: blue;
1010
}
1111
1212
<div
13-
className="emotion-2"
13+
className="emotion-0"
1414
>
1515
<div
16-
className="emotion-0 emotion-1"
16+
className="emotion-1 emotion-2"
1717
/>
1818
</div>
1919
`;

packages/css/test/__snapshots__/css.test.js.snap

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -439,38 +439,6 @@ exports[`css nested at rule 1`] = `
439439
/>
440440
`;
441441

442-
exports[`css nested at rules 1`] = `
443-
@media (min-width: 420px) {
444-
.glamor-0 {
445-
color: pink;
446-
}
447-
}
448-
449-
@media (min-width: 420px) and (max-width: 500px) {
450-
.glamor-0 {
451-
color: hotpink;
452-
}
453-
}
454-
455-
@supports (display: grid) {
456-
.glamor-0 {
457-
display: grid;
458-
}
459-
}
460-
461-
@supports (display: grid) and ((display: -webkit-box) or (display: flex)) {
462-
.glamor-0 {
463-
display: -webkit-box;
464-
display: -ms-flexbox;
465-
display: flex;
466-
}
467-
}
468-
469-
<div
470-
className="glamor-0"
471-
/>
472-
`;
473-
474442
exports[`css null rule 1`] = `
475443
<div
476444
className="emotion-0"

packages/jest/src/create-serializer.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@ function getNodes(node, nodes = []) {
2323
return nodes
2424
}
2525

26+
if (typeof node === 'object') {
27+
nodes.push(node)
28+
}
29+
2630
if (node.children) {
2731
for (let child of node.children) {
2832
getNodes(child, nodes)
2933
}
3034
}
3135

32-
if (typeof node === 'object') {
33-
nodes.push(node)
34-
}
35-
3636
return nodes
3737
}
3838

packages/jest/src/utils.js

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ export function findLast<T>(arr: T[], predicate: T => boolean) {
1818
}
1919
}
2020

21+
export function findIndexFrom<T>(
22+
arr: T[],
23+
fromIndex: number,
24+
predicate: T => boolean
25+
) {
26+
for (let i = fromIndex; i < arr.length; i++) {
27+
if (predicate(arr[i])) {
28+
return i
29+
}
30+
}
31+
32+
return -1
33+
}
34+
2135
function getClassNames(selectors: any, classes?: string) {
2236
return classes ? selectors.concat(classes.split(' ')) : selectors
2337
}
@@ -128,6 +142,19 @@ const getElementRules = (element: HTMLStyleElement): string[] => {
128142
return [].slice.call(element.sheet.cssRules).map(cssRule => cssRule.cssText)
129143
}
130144

145+
const getKeyframesMap = rules =>
146+
rules.reduce((keyframes, rule) => {
147+
const match = rule.match(keyframesPattern)
148+
if (match !== null) {
149+
const name = match[1]
150+
if (keyframes[name] === undefined) {
151+
keyframes[name] = ''
152+
}
153+
keyframes[name] += rule
154+
}
155+
return keyframes
156+
}, {})
157+
131158
export function getStylesFromClassNames(
132159
classNames: Array<string>,
133160
elements: Array<HTMLStyleElement>
@@ -155,25 +182,36 @@ export function getStylesFromClassNames(
155182
return ''
156183
}
157184
const selectorPattern = new RegExp(
158-
'\\.(' + filteredClassNames.join('|') + ')'
185+
'\\.(?:' + filteredClassNames.map(cls => `(${cls})`).join('|') + ')'
159186
)
160-
const keyframes = {}
161-
let styles = ''
162187

163-
flatMap(elements, getElementRules).forEach((rule: string) => {
164-
if (selectorPattern.test(rule)) {
165-
styles += rule
166-
}
167-
const match = rule.match(keyframesPattern)
168-
if (match !== null) {
169-
const name = match[1]
170-
if (keyframes[name] === undefined) {
171-
keyframes[name] = ''
188+
const rules = flatMap(elements, getElementRules)
189+
190+
let styles = rules
191+
.map((rule: string) => {
192+
const match = rule.match(selectorPattern)
193+
if (!match) {
194+
return null
172195
}
173-
keyframes[name] += rule
174-
}
175-
})
176-
const keyframeNameKeys = Object.keys(keyframes)
196+
// `selectorPattern` represents all emotion-generated class names
197+
// each possible class name is wrapped in a capturing group
198+
// and those groups appear in the same order as they appear in the DOM within class attributes
199+
// because we've gathered them from the DOM in such order
200+
// given that information we can sort matched rules based on the capturing group that has been matched
201+
// to end up with styles in a stable order
202+
const matchedCapturingGroupIndex = findIndexFrom(match, 1, Boolean)
203+
return [rule, matchedCapturingGroupIndex]
204+
})
205+
.filter(Boolean)
206+
.sort(
207+
([ruleA, classNameIndexA], [ruleB, classNameIndexB]) =>
208+
classNameIndexA - classNameIndexB
209+
)
210+
.map(([rule]) => rule)
211+
.join('')
212+
213+
const keyframesMap = getKeyframesMap(rules)
214+
const keyframeNameKeys = Object.keys(keyframesMap)
177215
let keyframesStyles = ''
178216

179217
if (keyframeNameKeys.length) {
@@ -184,7 +222,7 @@ export function getStylesFromClassNames(
184222
styles = styles.replace(keyframesNamePattern, name => {
185223
if (keyframesNameCache[name] === undefined) {
186224
keyframesNameCache[name] = `animation-${index++}`
187-
keyframesStyles += keyframes[name]
225+
keyframesStyles += keyframesMap[name]
188226
}
189227
return keyframesNameCache[name]
190228
})

packages/jest/test/__snapshots__/preact.test.js.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
exports[`jest-emotion with preact handles elements with no props 1`] = `"<div />"`;
44

55
exports[`jest-emotion with preact replaces class names and inserts styles into preact test component snapshots 1`] = `
6-
".emotion-1 {
6+
".emotion-0 {
77
color: red;
88
}
99
10-
.emotion-0 {
10+
.emotion-1 {
1111
width: 100%;
1212
}
1313
1414
<div
15-
class=\\"emotion-1\\"
15+
class=\\"emotion-0\\"
1616
>
1717
<svg
18-
class=\\"emotion-0\\"
18+
class=\\"emotion-1\\"
1919
/>
2020
</div>"
2121
`;

packages/jest/test/__snapshots__/printer.test.js.snap

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,55 +21,55 @@ exports[`jest-emotion with DOM elements disabled does not replace class names or
2121
`;
2222

2323
exports[`jest-emotion with DOM elements disabled replaces class names and inserts styles into React test component snapshots 1`] = `
24-
".emotion-1 {
24+
".emotion-0 {
2525
color: red;
2626
}
2727
28-
.emotion-0 {
28+
.emotion-1 {
2929
width: 100%;
3030
}
3131
3232
<div
33-
className=\\"emotion-1\\"
33+
className=\\"emotion-0\\"
3434
>
3535
<svg
36-
className=\\"emotion-0\\"
36+
className=\\"emotion-1\\"
3737
/>
3838
</div>"
3939
`;
4040

4141
exports[`jest-emotion with dom elements replaces class names and inserts styles into DOM element snapshots 1`] = `
42-
".emotion-1 {
42+
".emotion-0 {
4343
color: red;
4444
}
4545
46-
.emotion-0 {
46+
.emotion-1 {
4747
width: 100%;
4848
}
4949
5050
<div
51-
class=\\"emotion-1\\"
51+
class=\\"emotion-0\\"
5252
>
5353
<svg
54-
class=\\"emotion-0\\"
54+
class=\\"emotion-1\\"
5555
/>
5656
</div>"
5757
`;
5858

5959
exports[`jest-emotion with dom elements replaces class names and inserts styles into React test component snapshots 1`] = `
60-
".emotion-1 {
60+
".emotion-0 {
6161
color: red;
6262
}
6363
64-
.emotion-0 {
64+
.emotion-1 {
6565
width: 100%;
6666
}
6767
6868
<div
69-
className=\\"emotion-1\\"
69+
className=\\"emotion-0\\"
7070
>
7171
<svg
72-
className=\\"emotion-0\\"
72+
className=\\"emotion-1\\"
7373
/>
7474
</div>"
7575
`;

packages/react/__tests__/__snapshots__/use-theme.js.snap

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`Nested useTheme works 1`] = `
4-
.emotion-1 {
4+
.emotion-0 {
55
color: green;
66
}
77
8-
.emotion-1:hover {
8+
.emotion-0:hover {
99
color: darkgreen;
1010
}
1111
12-
.emotion-0 {
12+
.emotion-1 {
1313
color: lawngreen;
1414
}
1515
16-
.emotion-0:hover {
16+
.emotion-1:hover {
1717
color: seagreen;
1818
}
1919
2020
<div
21-
className="emotion-1"
21+
className="emotion-0"
2222
>
2323
Should be green
2424
<div
25-
className="emotion-0"
25+
className="emotion-1"
2626
>
2727
Should be lawngreen
2828
</div>

packages/styled/__tests__/__snapshots__/styled.js.snap

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,22 @@ exports[`styled call expression 1`] = `
4545

4646
exports[`styled component selectors 1`] = `
4747
.emotion-0 {
48-
color: hotpink;
49-
}
50-
51-
.emotion-2 {
5248
color: green;
5349
}
5450
55-
.emotion-2 .emotion-1 {
51+
.emotion-0 .emotion-2 {
5652
color: yellow;
5753
}
5854
55+
.emotion-1 {
56+
color: hotpink;
57+
}
58+
5959
<div
60-
className="emotion-2"
60+
className="emotion-0"
6161
>
6262
<div
63-
className="emotion-0 emotion-1"
63+
className="emotion-1 emotion-2"
6464
/>
6565
</div>
6666
`;
@@ -329,30 +329,30 @@ exports[`styled keyframes with css call 1`] = `
329329

330330
exports[`styled nested 1`] = `
331331
.emotion-0 {
332-
font-size: 20px;
333-
}
334-
335-
.emotion-1 {
336332
display: -webkit-box;
337333
display: -webkit-flex;
338334
display: -ms-flexbox;
339335
display: flex;
340336
}
341337
342-
.emotion-1 div {
338+
.emotion-0 div {
343339
color: green;
344340
}
345341
346-
.emotion-1 div span {
342+
.emotion-0 div span {
347343
color: red;
348344
}
349345
346+
.emotion-1 {
347+
font-size: 20px;
348+
}
349+
350350
<div
351-
className="emotion-1"
351+
className="emotion-0"
352352
>
353353
hello
354354
<h1
355-
className="emotion-0"
355+
className="emotion-1"
356356
>
357357
This will be green
358358
</h1>

0 commit comments

Comments
 (0)