Skip to content

Commit fceea7a

Browse files
feat: handle line endings in snapshot (#15708)
1 parent cbe7cfe commit fceea7a

File tree

9 files changed

+123
-19
lines changed

9 files changed

+123
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Features
44

55
- `[expect]` Have `Inverse` exportable ([#15704](https://github.com/jestjs/jest/pull/15704))
6+
- `[jest-snapshot]` Handle line endings in snapshots ([#15708](https://github.com/jestjs/jest/pull/15708))
67

78
## 30.0.3
89

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`handles test names with different line endings in snapshots 1`] = `
4+
"Test Suites: 1 passed, 1 total
5+
Tests: 3 passed, 3 total
6+
Snapshots: 3 passed, 3 total
7+
Time: <<REPLACED>>
8+
Ran all test suites."
9+
`;

e2e/__tests__/snapshot-crlf.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {extractSummary} from '../Utils';
9+
import runJest from '../runJest';
10+
11+
test('handles test names with different line endings in snapshots', () => {
12+
const result = runJest('snapshot-crlf');
13+
const {summary} = extractSummary(result.stderr);
14+
15+
expect(result.exitCode).toBe(0);
16+
expect(summary).toMatchSnapshot();
17+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`CR\\r 1`] = `1`;
4+
5+
exports[`CRLF\\r\\n 1`] = `2`;
6+
7+
exports[`LF\\n 1`] = `3`;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
test('CR\r', () => {
8+
expect(1).toMatchSnapshot();
9+
});
10+
11+
test('CRLF\r\n', () => {
12+
expect(2).toMatchSnapshot();
13+
});
14+
15+
test('LF\n', () => {
16+
expect(3).toMatchSnapshot();
17+
});

e2e/snapshot-crlf/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"jest": {
3+
"testEnvironment": "node"
4+
}
5+
}

packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -292,12 +292,7 @@ Expected: <g>"cde"</>
292292
Received: <r>"abc"</>
293293
`;
294294

295-
exports[`.toBe() fails for: "four
296-
4
297-
line
298-
string" and "3
299-
line
300-
string" 1`] = `
295+
exports[`.toBe() fails for: "four\\n4\\nline\\nstring" and "3\\nline\\nstring" 1`] = `
301296
<d>expect(</><r>received</><d>).</>toBe<d>(</><g>expected</><d>) // Object.is equality</>
302297

303298
<g>- Expected - 1</>
@@ -317,8 +312,7 @@ Expected: <g>"<i>delightful</i> JavaScript testing"</>
317312
Received: <r>"<i>painless</i> JavaScript testing"</>
318313
`;
319314

320-
exports[`.toBe() fails for: "with
321-
trailing space" and "without trailing space" 1`] = `
315+
exports[`.toBe() fails for: "with \\ntrailing space" and "without trailing space" 1`] = `
322316
<d>expect(</><r>received</><d>).</>toBe<d>(</><g>expected</><d>) // Object.is equality</>
323317

324318
<g>- Expected - 1</>
@@ -2050,9 +2044,7 @@ Expected: <g>"apple"</>
20502044
Received: <r>"banana"</>
20512045
`;
20522046

2053-
exports[`.toEqual() {pass: false} expect("type TypeName<T> = T extends Function ? \\"function\\" : \\"object\\";").toEqual("type TypeName<T> = T extends Function
2054-
? \\"function\\"
2055-
: \\"object\\";") 1`] = `
2047+
exports[`.toEqual() {pass: false} expect("type TypeName<T> = T extends Function ? \\"function\\" : \\"object\\";").toEqual("type TypeName<T> = T extends Function\\n? \\"function\\"\\n: \\"object\\";") 1`] = `
20562048
<d>expect(</><r>received</><d>).</>toEqual<d>(</><g>expected</><d>) // deep equality</>
20572049

20582050
<g>- Expected - 3</>
@@ -3533,11 +3525,7 @@ Expected value: <g>"\\"That <i>cat </i>cartoon\\""</>
35333525
Received value: <r>"\\"That cartoon\\""</>
35343526
`;
35353527

3536-
exports[`.toHaveProperty() {pass: false} expect({"children": ["Roses are red.
3537-
Violets are blue.
3538-
Testing with Jest is good for you."], "props": null, "type": "pre"}).toHaveProperty('children,0', "Roses are red, violets are blue.
3539-
Testing with Jest
3540-
Is good for you.") 1`] = `
3528+
exports[`.toHaveProperty() {pass: false} expect({"children": ["Roses are red.\\nViolets are blue.\\nTesting with Jest is good for you."], "props": null, "type": "pre"}).toHaveProperty('children,0', "Roses are red, violets are blue.\\nTesting with Jest\\nIs good for you.") 1`] = `
35413529
<d>expect(</><r>received</><d>).</>toHaveProperty<d>(</><g>path</><d>, </><g>value</><d>)</>
35423530

35433531
Expected path: <g>["children", 0]</>

packages/jest-snapshot-utils/src/__tests__/utils.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import {
2626
test('keyToTestName()', () => {
2727
expect(keyToTestName('abc cde 12')).toBe('abc cde');
2828
expect(keyToTestName('abc cde 12')).toBe('abc cde ');
29+
expect(keyToTestName('test with\\r\\nCRLF 1')).toBe('test with\r\nCRLF');
30+
expect(keyToTestName('test with\\rCR 1')).toBe('test with\rCR');
31+
expect(keyToTestName('test with\\nLF 1')).toBe('test with\nLF');
2932
expect(() => keyToTestName('abc cde')).toThrow(
3033
'Snapshot keys must end with a number.',
3134
);
@@ -36,6 +39,35 @@ test('testNameToKey', () => {
3639
expect(testNameToKey('abc cde ', 12)).toBe('abc cde 12');
3740
});
3841

42+
test('testNameToKey escapes line endings to prevent collisions', () => {
43+
expect(testNameToKey('test with\r\nCRLF', 1)).toBe('test with\\r\\nCRLF 1');
44+
expect(testNameToKey('test with\rCR', 1)).toBe('test with\\rCR 1');
45+
expect(testNameToKey('test with\nLF', 1)).toBe('test with\\nLF 1');
46+
47+
expect(testNameToKey('test\r\n', 1)).not.toBe(testNameToKey('test\r', 1));
48+
expect(testNameToKey('test\r\n', 1)).not.toBe(testNameToKey('test\n', 1));
49+
expect(testNameToKey('test\r', 1)).not.toBe(testNameToKey('test\n', 1));
50+
});
51+
52+
test('keyToTestName reverses testNameToKey transformation', () => {
53+
const testCases = [
54+
'simple test',
55+
'test with\r\nCRLF',
56+
'test with\rCR only',
57+
'test with\nLF only',
58+
'mixed\r\nline\rendings\n',
59+
'test\r',
60+
'test\r\n',
61+
'test\n',
62+
];
63+
64+
for (const testName of testCases) {
65+
const key = testNameToKey(testName, 1);
66+
const recovered = keyToTestName(key);
67+
expect(recovered).toBe(testName);
68+
}
69+
});
70+
3971
test('saveSnapshotFile() works with \r\n', () => {
4072
const filename = path.join(__dirname, 'remove-newlines.snap');
4173
const data = {

packages/jest-snapshot-utils/src/utils.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,43 @@ const validateSnapshotVersion = (snapshotContents: string) => {
7575
return null;
7676
};
7777

78+
const normalizeTestNameForKey = (testName: string): string =>
79+
testName.replaceAll(/\r\n|\r|\n/g, match => {
80+
switch (match) {
81+
case '\r\n':
82+
return '\\r\\n';
83+
case '\r':
84+
return '\\r';
85+
case '\n':
86+
return '\\n';
87+
default:
88+
return match;
89+
}
90+
});
91+
92+
const denormalizeTestNameFromKey = (key: string): string =>
93+
key.replaceAll(/\\r\\n|\\r|\\n/g, match => {
94+
switch (match) {
95+
case '\\r\\n':
96+
return '\r\n';
97+
case '\\r':
98+
return '\r';
99+
case '\\n':
100+
return '\n';
101+
default:
102+
return match;
103+
}
104+
});
105+
78106
export const testNameToKey = (testName: string, count: number): string =>
79-
`${testName} ${count}`;
107+
`${normalizeTestNameForKey(testName)} ${count}`;
80108

81109
export const keyToTestName = (key: string): string => {
82110
if (!/ \d+$/.test(key)) {
83111
throw new Error('Snapshot keys must end with a number.');
84112
}
85-
86-
return key.replace(/ \d+$/, '');
113+
const testNameWithoutCount = key.replace(/ \d+$/, '');
114+
return denormalizeTestNameFromKey(testNameWithoutCount);
87115
};
88116

89117
export const getSnapshotData = (

0 commit comments

Comments
 (0)