feat: handle line endings in snapshot (#15708)

This commit is contained in:
Seong Min Park
2025-07-03 08:20:27 +09:00
committed by GitHub
parent 7d271d51d1
commit 0903be342c
9 changed files with 123 additions and 19 deletions
+1
View File
@@ -3,6 +3,7 @@
### Features
- `[expect]` Have `Inverse` exportable ([#15704](https://github.com/jestjs/jest/pull/15704))
- `[jest-snapshot]` Handle line endings in snapshots ([#15708](https://github.com/jestjs/jest/pull/15708))
## 30.0.3
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`handles test names with different line endings in snapshots 1`] = `
"Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 3 passed, 3 total
Time: <<REPLACED>>
Ran all test suites."
`;
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {extractSummary} from '../Utils';
import runJest from '../runJest';
test('handles test names with different line endings in snapshots', () => {
const result = runJest('snapshot-crlf');
const {summary} = extractSummary(result.stderr);
expect(result.exitCode).toBe(0);
expect(summary).toMatchSnapshot();
});
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`CR\\r 1`] = `1`;
exports[`CRLF\\r\\n 1`] = `2`;
exports[`LF\\n 1`] = `3`;
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
test('CR\r', () => {
expect(1).toMatchSnapshot();
});
test('CRLF\r\n', () => {
expect(2).toMatchSnapshot();
});
test('LF\n', () => {
expect(3).toMatchSnapshot();
});
+5
View File
@@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}
@@ -292,12 +292,7 @@ Expected: <g>"cde"</>
Received: <r>"abc"</>
`;
exports[`.toBe() fails for: "four
4
line
string" and "3
line
string" 1`] = `
exports[`.toBe() fails for: "four\\n4\\nline\\nstring" and "3\\nline\\nstring" 1`] = `
<d>expect(</><r>received</><d>).</>toBe<d>(</><g>expected</><d>) // Object.is equality</>
<g>- Expected - 1</>
@@ -317,8 +312,7 @@ Expected: <g>"<i>delightful</i> JavaScript testing"</>
Received: <r>"<i>painless</i> JavaScript testing"</>
`;
exports[`.toBe() fails for: "with
trailing space" and "without trailing space" 1`] = `
exports[`.toBe() fails for: "with \\ntrailing space" and "without trailing space" 1`] = `
<d>expect(</><r>received</><d>).</>toBe<d>(</><g>expected</><d>) // Object.is equality</>
<g>- Expected - 1</>
@@ -2050,9 +2044,7 @@ Expected: <g>"apple"</>
Received: <r>"banana"</>
`;
exports[`.toEqual() {pass: false} expect("type TypeName<T> = T extends Function ? \\"function\\" : \\"object\\";").toEqual("type TypeName<T> = T extends Function
? \\"function\\"
: \\"object\\";") 1`] = `
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`] = `
<d>expect(</><r>received</><d>).</>toEqual<d>(</><g>expected</><d>) // deep equality</>
<g>- Expected - 3</>
@@ -3533,11 +3525,7 @@ Expected value: <g>"\\"That <i>cat </i>cartoon\\""</>
Received value: <r>"\\"That cartoon\\""</>
`;
exports[`.toHaveProperty() {pass: false} expect({"children": ["Roses are red.
Violets are blue.
Testing with Jest is good for you."], "props": null, "type": "pre"}).toHaveProperty('children,0', "Roses are red, violets are blue.
Testing with Jest
Is good for you.") 1`] = `
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`] = `
<d>expect(</><r>received</><d>).</>toHaveProperty<d>(</><g>path</><d>, </><g>value</><d>)</>
Expected path: <g>["children", 0]</>
@@ -26,6 +26,9 @@ import {
test('keyToTestName()', () => {
expect(keyToTestName('abc cde 12')).toBe('abc cde');
expect(keyToTestName('abc cde 12')).toBe('abc cde ');
expect(keyToTestName('test with\\r\\nCRLF 1')).toBe('test with\r\nCRLF');
expect(keyToTestName('test with\\rCR 1')).toBe('test with\rCR');
expect(keyToTestName('test with\\nLF 1')).toBe('test with\nLF');
expect(() => keyToTestName('abc cde')).toThrow(
'Snapshot keys must end with a number.',
);
@@ -36,6 +39,35 @@ test('testNameToKey', () => {
expect(testNameToKey('abc cde ', 12)).toBe('abc cde 12');
});
test('testNameToKey escapes line endings to prevent collisions', () => {
expect(testNameToKey('test with\r\nCRLF', 1)).toBe('test with\\r\\nCRLF 1');
expect(testNameToKey('test with\rCR', 1)).toBe('test with\\rCR 1');
expect(testNameToKey('test with\nLF', 1)).toBe('test with\\nLF 1');
expect(testNameToKey('test\r\n', 1)).not.toBe(testNameToKey('test\r', 1));
expect(testNameToKey('test\r\n', 1)).not.toBe(testNameToKey('test\n', 1));
expect(testNameToKey('test\r', 1)).not.toBe(testNameToKey('test\n', 1));
});
test('keyToTestName reverses testNameToKey transformation', () => {
const testCases = [
'simple test',
'test with\r\nCRLF',
'test with\rCR only',
'test with\nLF only',
'mixed\r\nline\rendings\n',
'test\r',
'test\r\n',
'test\n',
];
for (const testName of testCases) {
const key = testNameToKey(testName, 1);
const recovered = keyToTestName(key);
expect(recovered).toBe(testName);
}
});
test('saveSnapshotFile() works with \r\n', () => {
const filename = path.join(__dirname, 'remove-newlines.snap');
const data = {
+31 -3
View File
@@ -75,15 +75,43 @@ const validateSnapshotVersion = (snapshotContents: string) => {
return null;
};
const normalizeTestNameForKey = (testName: string): string =>
testName.replaceAll(/\r\n|\r|\n/g, match => {
switch (match) {
case '\r\n':
return '\\r\\n';
case '\r':
return '\\r';
case '\n':
return '\\n';
default:
return match;
}
});
const denormalizeTestNameFromKey = (key: string): string =>
key.replaceAll(/\\r\\n|\\r|\\n/g, match => {
switch (match) {
case '\\r\\n':
return '\r\n';
case '\\r':
return '\r';
case '\\n':
return '\n';
default:
return match;
}
});
export const testNameToKey = (testName: string, count: number): string =>
`${testName} ${count}`;
`${normalizeTestNameForKey(testName)} ${count}`;
export const keyToTestName = (key: string): string => {
if (!/ \d+$/.test(key)) {
throw new Error('Snapshot keys must end with a number.');
}
return key.replace(/ \d+$/, '');
const testNameWithoutCount = key.replace(/ \d+$/, '');
return denormalizeTestNameFromKey(testNameWithoutCount);
};
export const getSnapshotData = (