Skip to content

Commit 7a2ccc2

Browse files
authored
feat: split some jest-snapshot utility functions to its own package @jest/snapshot-utils (#15095)
1 parent b7ae0b8 commit 7a2ccc2

File tree

19 files changed

+445
-344
lines changed

19 files changed

+445
-344
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
- `[@jest/types]` Improve argument type inference passed to `test` and `describe` callback functions from `each` tables ([#14920](https://github.com/jestjs/jest/pull/14920))
3535
- `[jest-snapshot]` [**BREAKING**] Add support for [Error causes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) in snapshots ([#13965](https://github.com/facebook/jest/pull/13965))
3636
- `[jest-snapshot]` Support Prettier 3 ([#14566](https://github.com/facebook/jest/pull/14566))
37+
- `[@jest/util-snapshot]` Extract utils used by tooling from `jest-snapshot` into its own package ([#15095](https://github.com/facebook/jest/pull/15095))
3738
- `[pretty-format]` [**BREAKING**] Do not render empty string children (`''`) in React plugin ([#14470](https://github.com/facebook/jest/pull/14470))
3839

3940
### Fixes

e2e/__tests__/findRelatedFiles.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ afterEach(() => cleanup(DIR));
1818
describe('--findRelatedTests flag', () => {
1919
test('runs tests related to filename', () => {
2020
writeFiles(DIR, {
21-
'.watchmanconfig': '',
21+
'.watchmanconfig': '{}',
2222
'__tests__/test.test.js': `
2323
const a = require('../a');
2424
test('a', () => {});
@@ -44,7 +44,7 @@ describe('--findRelatedTests flag', () => {
4444
}
4545

4646
writeFiles(DIR, {
47-
'.watchmanconfig': '',
47+
'.watchmanconfig': '{}',
4848
'__tests__/test.test.js': `
4949
const a = require('../a');
5050
test('a', () => {});
@@ -65,7 +65,7 @@ describe('--findRelatedTests flag', () => {
6565

6666
test('runs tests related to filename with a custom dependency extractor', () => {
6767
writeFiles(DIR, {
68-
'.watchmanconfig': '',
68+
'.watchmanconfig': '{}',
6969
'__tests__/test-skip-deps.test.js': `
7070
const dynamicImport = path => Promise.resolve(require(path));
7171
test('a', () => dynamicImport('../a').then(a => {
@@ -118,7 +118,7 @@ describe('--findRelatedTests flag', () => {
118118

119119
test('runs tests related to filename with a custom dependency extractor written in ESM', () => {
120120
writeFiles(DIR, {
121-
'.watchmanconfig': '',
121+
'.watchmanconfig': '{}',
122122
'__tests__/test-skip-deps.test.js': `
123123
const dynamicImport = path => Promise.resolve(require(path));
124124
test('a', () => dynamicImport('../a').then(a => {
@@ -168,7 +168,7 @@ describe('--findRelatedTests flag', () => {
168168

169169
test('generates coverage report for filename', () => {
170170
writeFiles(DIR, {
171-
'.watchmanconfig': '',
171+
'.watchmanconfig': '{}',
172172
'__tests__/a.test.js': `
173173
require('../a');
174174
require('../b');
@@ -219,7 +219,7 @@ describe('--findRelatedTests flag', () => {
219219

220220
test('coverage configuration is applied correctly', () => {
221221
writeFiles(DIR, {
222-
'.watchmanconfig': '',
222+
'.watchmanconfig': '{}',
223223
'__tests__/a.test.js': `
224224
require('../a');
225225
test('a', () => expect(1).toBe(1));

e2e/__tests__/multiProjectRunner.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ afterEach(() => cleanup(DIR));
1919

2020
test("--listTests doesn't duplicate the test files", () => {
2121
writeFiles(DIR, {
22-
'.watchmanconfig': '',
22+
'.watchmanconfig': '{}',
2323
'/project1.js': "module.exports = {rootDir: './', displayName: 'BACKEND'}",
2424
'/project2.js': "module.exports = {rootDir: './', displayName: 'BACKEND'}",
2525
'__tests__/inBothProjectsTest.js': "test('test', () => {});",
@@ -35,7 +35,7 @@ test("--listTests doesn't duplicate the test files", () => {
3535

3636
test('can pass projects or global config', () => {
3737
writeFiles(DIR, {
38-
'.watchmanconfig': '',
38+
'.watchmanconfig': '{}',
3939
'base_config.js': `
4040
module.exports = {
4141
haste: {
@@ -132,7 +132,7 @@ test('can pass projects or global config', () => {
132132

133133
test('"No tests found" message for projects', () => {
134134
writeFiles(DIR, {
135-
'.watchmanconfig': '',
135+
'.watchmanconfig': '{}',
136136
'package.json': '{}',
137137
'project1/__tests__/file1.test.js': `
138138
const file1 = require('../file1');
@@ -336,7 +336,7 @@ test('allows a single project', () => {
336336

337337
test('resolves projects and their <rootDir> properly', () => {
338338
writeFiles(DIR, {
339-
'.watchmanconfig': '',
339+
'.watchmanconfig': '{}',
340340
'package.json': JSON.stringify({
341341
jest: {
342342
projects: [
@@ -436,7 +436,7 @@ test('resolves projects and their <rootDir> properly', () => {
436436

437437
test('Does transform files with the corresponding project transformer', () => {
438438
writeFiles(DIR, {
439-
'.watchmanconfig': '',
439+
'.watchmanconfig': '{}',
440440
'file.js': SAMPLE_FILE_CONTENT,
441441
'package.json': '{}',
442442
'project1/__tests__/project1.test.js': `
@@ -487,7 +487,7 @@ test('Does transform files with the corresponding project transformer', () => {
487487
describe("doesn't bleed module file extensions resolution with multiple workers", () => {
488488
test('external config files', () => {
489489
writeFiles(DIR, {
490-
'.watchmanconfig': '',
490+
'.watchmanconfig': '{}',
491491
'file.js': 'module.exports = "file1"',
492492
'file.p2.js': 'module.exports = "file2"',
493493
'package.json': '{}',
@@ -537,7 +537,7 @@ describe("doesn't bleed module file extensions resolution with multiple workers"
537537

538538
test('inline config files', () => {
539539
writeFiles(DIR, {
540-
'.watchmanconfig': '',
540+
'.watchmanconfig': '{}',
541541
'file.js': 'module.exports = "file1"',
542542
'file.p2.js': 'module.exports = "file2"',
543543
'package.json': JSON.stringify({

e2e/__tests__/unexpectedToken.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ afterEach(() => cleanup(DIR));
1717

1818
test('triggers unexpected token error message for non-JS assets', () => {
1919
writeFiles(DIR, {
20-
'.watchmanconfig': '',
20+
'.watchmanconfig': '{}',
2121
'asset.css': '.style {}',
2222
'package.json': JSON.stringify({jest: {testEnvironment: 'node'}}),
2323
});
@@ -37,7 +37,7 @@ test('triggers unexpected token error message for non-JS assets', () => {
3737

3838
test('triggers unexpected token error message for untranspiled node_modules', () => {
3939
writeFiles(DIR, {
40-
'.watchmanconfig': '',
40+
'.watchmanconfig': '{}',
4141
'node_modules/untranspiled-module': 'import {module} from "some-module"',
4242
'package.json': JSON.stringify({jest: {testEnvironment: 'node'}}),
4343
});
@@ -59,7 +59,7 @@ test('triggers unexpected token error message for untranspiled node_modules', ()
5959

6060
test('does not trigger unexpected token error message for regular syntax errors', () => {
6161
writeFiles(DIR, {
62-
'.watchmanconfig': '',
62+
'.watchmanconfig': '{}',
6363
'faulty.js': 'import {module from "some-module"',
6464
'faulty2.js': 'const name = {first: "Name" second: "Second"}',
6565
'package.json': JSON.stringify({jest: {testEnvironment: 'node'}}),
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@jest/snapshot-utils",
3+
"version": "30.0.0-alpha.4",
4+
"repository": {
5+
"type": "git",
6+
"url": "https://github.com/jestjs/jest.git",
7+
"directory": "packages/jest-snapshot-utils"
8+
},
9+
"license": "MIT",
10+
"main": "./build/index.js",
11+
"types": "./build/index.d.ts",
12+
"exports": {
13+
".": {
14+
"types": "./build/index.d.ts",
15+
"require": "./build/index.js",
16+
"import": "./build/index.mjs",
17+
"default": "./build/index.js"
18+
},
19+
"./package.json": "./package.json"
20+
},
21+
"dependencies": {
22+
"@jest/types": "workspace:*",
23+
"chalk": "^4.0.0",
24+
"graceful-fs": "^4.2.9",
25+
"natural-compare": "^1.4.0"
26+
},
27+
"devDependencies": {
28+
"@types/graceful-fs": "^4.1.3",
29+
"@types/natural-compare": "^1.4.0"
30+
},
31+
"engines": {
32+
"node": "^16.10.0 || ^18.12.0 || >=20.0.0"
33+
},
34+
"publishConfig": {
35+
"access": "public"
36+
}
37+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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+
jest.mock('graceful-fs', () => ({
9+
...jest.createMockFromModule<typeof import('fs')>('fs'),
10+
existsSync: jest.fn().mockReturnValue(true),
11+
}));
12+
13+
import * as path from 'path';
14+
import chalk = require('chalk');
15+
import * as fs from 'graceful-fs';
16+
import {
17+
SNAPSHOT_GUIDE_LINK,
18+
SNAPSHOT_VERSION,
19+
SNAPSHOT_VERSION_WARNING,
20+
getSnapshotData,
21+
keyToTestName,
22+
saveSnapshotFile,
23+
testNameToKey,
24+
} from '../utils';
25+
26+
test('keyToTestName()', () => {
27+
expect(keyToTestName('abc cde 12')).toBe('abc cde');
28+
expect(keyToTestName('abc cde 12')).toBe('abc cde ');
29+
expect(() => keyToTestName('abc cde')).toThrow(
30+
'Snapshot keys must end with a number.',
31+
);
32+
});
33+
34+
test('testNameToKey', () => {
35+
expect(testNameToKey('abc cde', 1)).toBe('abc cde 1');
36+
expect(testNameToKey('abc cde ', 12)).toBe('abc cde 12');
37+
});
38+
39+
test('saveSnapshotFile() works with \r\n', () => {
40+
const filename = path.join(__dirname, 'remove-newlines.snap');
41+
const data = {
42+
myKey: '<div>\r\n</div>',
43+
};
44+
45+
saveSnapshotFile(data, filename);
46+
expect(fs.writeFileSync).toHaveBeenCalledWith(
47+
filename,
48+
`// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` +
49+
'exports[`myKey`] = `<div>\n</div>`;\n',
50+
);
51+
});
52+
53+
test('saveSnapshotFile() works with \r', () => {
54+
const filename = path.join(__dirname, 'remove-newlines.snap');
55+
const data = {
56+
myKey: '<div>\r</div>',
57+
};
58+
59+
saveSnapshotFile(data, filename);
60+
expect(fs.writeFileSync).toHaveBeenCalledWith(
61+
filename,
62+
`// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` +
63+
'exports[`myKey`] = `<div>\n</div>`;\n',
64+
);
65+
});
66+
67+
test('getSnapshotData() throws when no snapshot version', () => {
68+
const filename = path.join(__dirname, 'old-snapshot.snap');
69+
jest
70+
.mocked(fs.readFileSync)
71+
.mockReturnValue('exports[`myKey`] = `<div>\n</div>`;\n');
72+
const update = 'none';
73+
74+
expect(() => getSnapshotData(filename, update)).toThrow(
75+
chalk.red(
76+
`${chalk.bold('Outdated snapshot')}: No snapshot header found. ` +
77+
'Jest 19 introduced versioned snapshots to ensure all developers on ' +
78+
'a project are using the same version of Jest. ' +
79+
'Please update all snapshots during this upgrade of Jest.\n\n',
80+
) + SNAPSHOT_VERSION_WARNING,
81+
);
82+
});
83+
84+
test('getSnapshotData() throws for older snapshot version', () => {
85+
const filename = path.join(__dirname, 'old-snapshot.snap');
86+
jest
87+
.mocked(fs.readFileSync)
88+
.mockReturnValue(
89+
`// Jest Snapshot v0.99, ${SNAPSHOT_GUIDE_LINK}\n\n` +
90+
'exports[`myKey`] = `<div>\n</div>`;\n',
91+
);
92+
const update = 'none';
93+
94+
expect(() => getSnapshotData(filename, update)).toThrow(
95+
`${chalk.red(
96+
`${chalk.red.bold('Outdated snapshot')}: The version of the snapshot ` +
97+
'file associated with this test is outdated. The snapshot file ' +
98+
'version ensures that all developers on a project are using ' +
99+
'the same version of Jest. ' +
100+
'Please update all snapshots during this upgrade of Jest.',
101+
)}\n\nExpected: v${SNAPSHOT_VERSION}\n` +
102+
`Received: v0.99\n\n${SNAPSHOT_VERSION_WARNING}`,
103+
);
104+
});
105+
106+
test('getSnapshotData() throws for newer snapshot version', () => {
107+
const filename = path.join(__dirname, 'old-snapshot.snap');
108+
jest
109+
.mocked(fs.readFileSync)
110+
.mockReturnValue(
111+
`// Jest Snapshot v2, ${SNAPSHOT_GUIDE_LINK}\n\n` +
112+
'exports[`myKey`] = `<div>\n</div>`;\n',
113+
);
114+
const update = 'none';
115+
116+
expect(() => getSnapshotData(filename, update)).toThrow(
117+
`${chalk.red(
118+
`${chalk.red.bold('Outdated Jest version')}: The version of this ` +
119+
'snapshot file indicates that this project is meant to be used ' +
120+
'with a newer version of Jest. ' +
121+
'The snapshot file version ensures that all developers on a project ' +
122+
'are using the same version of Jest. ' +
123+
'Please update your version of Jest and re-run the tests.',
124+
)}\n\nExpected: v${SNAPSHOT_VERSION}\nReceived: v2`,
125+
);
126+
});
127+
128+
test('getSnapshotData() does not throw for when updating', () => {
129+
const filename = path.join(__dirname, 'old-snapshot.snap');
130+
jest
131+
.mocked(fs.readFileSync)
132+
.mockReturnValue('exports[`myKey`] = `<div>\n</div>`;\n');
133+
const update = 'all';
134+
135+
expect(() => getSnapshotData(filename, update)).not.toThrow();
136+
});
137+
138+
test('getSnapshotData() marks invalid snapshot dirty when updating', () => {
139+
const filename = path.join(__dirname, 'old-snapshot.snap');
140+
jest
141+
.mocked(fs.readFileSync)
142+
.mockReturnValue('exports[`myKey`] = `<div>\n</div>`;\n');
143+
const update = 'all';
144+
145+
expect(getSnapshotData(filename, update)).toMatchObject({dirty: true});
146+
});
147+
148+
test('getSnapshotData() marks valid snapshot not dirty when updating', () => {
149+
const filename = path.join(__dirname, 'old-snapshot.snap');
150+
jest
151+
.mocked(fs.readFileSync)
152+
.mockReturnValue(
153+
`// Jest Snapshot v${SNAPSHOT_VERSION}, ${SNAPSHOT_GUIDE_LINK}\n\n` +
154+
'exports[`myKey`] = `<div>\n</div>`;\n',
155+
);
156+
const update = 'all';
157+
158+
expect(getSnapshotData(filename, update)).toMatchObject({dirty: false});
159+
});
160+
161+
test('escaping', () => {
162+
const filename = path.join(__dirname, 'escaping.snap');
163+
const data = '"\'\\';
164+
const writeFileSync = jest.mocked(fs.writeFileSync);
165+
166+
writeFileSync.mockReset();
167+
saveSnapshotFile({key: data}, filename);
168+
const writtenData = writeFileSync.mock.calls[0][1];
169+
expect(writtenData).toBe(
170+
`// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` +
171+
'exports[`key`] = `"\'\\\\`;\n',
172+
);
173+
174+
// eslint-disable-next-line no-eval
175+
const readData = eval(`var exports = {}; ${writtenData} exports`);
176+
expect(readData).toEqual({key: data});
177+
const snapshotData = readData.key;
178+
expect(data).toEqual(snapshotData);
179+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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+
export * from './utils';
9+
export * from './types';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
export type SnapshotData = Record<string, string>;

0 commit comments

Comments
 (0)