Skip to content

Commit d19c3af

Browse files
committed
feat(snapshot): support snapshotResolver and snapshotSerializers written in ESM
1 parent 27b89ec commit d19c3af

File tree

18 files changed

+372
-42
lines changed

18 files changed

+372
-42
lines changed

e2e/__tests__/circusDeclarationErrors.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import runJest from '../runJest';
1212

1313
skipSuiteOnJasmine();
1414

15-
it('defining tests and hooks asynchronously throws', () => {
16-
const result = runJest('circus-declaration-errors', [
15+
it('defining tests and hooks asynchronously throws', async () => {
16+
const result = await runJest('circus-declaration-errors', [
1717
'asyncDefinition.test.js',
1818
]);
1919

e2e/__tests__/transform.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,4 +347,42 @@ onNodeVersions('>=12.17.0', () => {
347347
expect(json.numPassedTests).toBe(1);
348348
});
349349
});
350+
351+
describe('transform-esm-snapshotResolver', () => {
352+
const dir = path.resolve(
353+
__dirname,
354+
'..',
355+
'transform/transform-esm-snapshotResolver',
356+
);
357+
const snapshotDir = path.resolve(dir, '__snapshots__');
358+
const snapshotFile = path.resolve(snapshotDir, 'snapshot.test.js.snap');
359+
360+
const cleanupTest = () => {
361+
if (fs.existsSync(snapshotFile)) {
362+
fs.unlinkSync(snapshotFile);
363+
}
364+
if (fs.existsSync(snapshotDir)) {
365+
fs.rmdirSync(snapshotDir);
366+
}
367+
};
368+
369+
beforeAll(() => {
370+
runYarnInstall(dir);
371+
});
372+
beforeEach(cleanupTest);
373+
afterAll(cleanupTest);
374+
375+
it('should transform the snapshotResolver', () => {
376+
const result = runJest(dir, ['-w=1', '--no-cache', '--ci=false'], {
377+
nodeOptions: '--experimental-vm-modules --no-warnings',
378+
});
379+
380+
expect(result.stderr).toMatch('1 snapshot written from 1 test suite');
381+
382+
const contents = require(snapshotFile);
383+
expect(contents).toHaveProperty(
384+
'snapshots are written to custom location 1',
385+
);
386+
});
387+
});
350388
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
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+
'use strict';
9+
10+
describe('snapshot serializers', () => {
11+
it('works with first plugin', () => {
12+
const test = {
13+
foo: 1,
14+
};
15+
expect(test).toMatchSnapshot();
16+
});
17+
18+
it('works with second plugin', () => {
19+
const test = {
20+
bar: 2,
21+
};
22+
expect(test).toMatchSnapshot();
23+
});
24+
25+
it('works with nested serializable objects', () => {
26+
const test = {
27+
foo: {
28+
bar: 2,
29+
},
30+
};
31+
expect(test).toMatchSnapshot();
32+
});
33+
34+
it('works with default serializers', () => {
35+
const test = {
36+
$$typeof: Symbol.for('react.test.json'),
37+
children: null,
38+
props: {
39+
id: 'foo',
40+
},
41+
type: 'div',
42+
};
43+
expect(test).toMatchSnapshot();
44+
});
45+
46+
it('works with prepended plugins and default serializers', () => {
47+
const test = {
48+
$$typeof: Symbol.for('react.test.json'),
49+
children: null,
50+
props: {
51+
aProp: {a: 6},
52+
bProp: {foo: 8},
53+
},
54+
type: 'div',
55+
};
56+
expect(test).toMatchSnapshot();
57+
});
58+
59+
it('works with prepended plugins from expect method called once', () => {
60+
const test = {
61+
$$typeof: Symbol.for('react.test.json'),
62+
children: null,
63+
props: {
64+
aProp: {a: 6},
65+
bProp: {foo: 8},
66+
},
67+
type: 'div',
68+
};
69+
// Add plugin that overrides foo specified by Jest config in package.json
70+
expect.addSnapshotSerializer({
71+
print: (val, serialize) => `Foo: ${serialize(val.foo)}`,
72+
test: val => val && val.hasOwnProperty('foo'),
73+
});
74+
expect(test).toMatchSnapshot();
75+
});
76+
77+
it('works with prepended plugins from expect method called twice', () => {
78+
const test = {
79+
$$typeof: Symbol.for('react.test.json'),
80+
children: null,
81+
props: {
82+
aProp: {a: 6},
83+
bProp: {foo: 8},
84+
},
85+
type: 'div',
86+
};
87+
// Add plugin that overrides preceding added plugin
88+
expect.addSnapshotSerializer({
89+
print: (val, serialize) => `FOO: ${serialize(val.foo)}`,
90+
test: val => val && val.hasOwnProperty('foo'),
91+
});
92+
expect(test).toMatchSnapshot();
93+
});
94+
95+
it('works with array of strings in property matcher', () => {
96+
expect({
97+
arrayOfStrings: ['stream'],
98+
}).toMatchSnapshot({
99+
arrayOfStrings: ['stream'],
100+
});
101+
});
102+
103+
it('works with expect.XXX within array in property matcher', () => {
104+
expect({
105+
arrayOfStrings: ['stream'],
106+
}).toMatchSnapshot({
107+
arrayOfStrings: [expect.any(String)],
108+
});
109+
});
110+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"jest": {
3+
"testEnvironment": "node",
4+
"transform": {
5+
"\\.js$": "<rootDir>/transformer.js"
6+
},
7+
"snapshotSerializers": [
8+
"./plugins/foo",
9+
"<rootDir>/plugins/bar"
10+
]
11+
}
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
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+
9+
import {createPlugin} from '../utils';
10+
11+
// We inject the call to "createPlugin('bar') through the transformer"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
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+
9+
import {createPlugin} from '../../utils';
10+
export default createPlugin('foo');
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
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+
'use strict';
9+
10+
module.exports = {
11+
process(src, filename) {
12+
if (/bar.mjs$/.test(filename)) {
13+
return `${src};\nexport default createPlugin('bar');`;
14+
}
15+
return src;
16+
},
17+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
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+
9+
export const createPlugin = prop => ({
10+
print: (val, serialize) => `${prop} - ${serialize(val[prop])}`,
11+
test: val => val && val.hasOwnProperty(prop),
12+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
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+
test('snapshots are written to custom location', () => {
9+
expect('foobar').toMatchSnapshot();
10+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
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 default {
9+
resolveSnapshotPath: (testPath, snapshotExtension) =>
10+
testPath.replace('__tests__', '__snapshots__') + snapshotExtension,
11+
12+
resolveTestPath: (snapshotFilePath, snapshotExtension) =>
13+
snapshotFilePath
14+
.replace('__snapshots__', '__tests__')
15+
.slice(0, -(snapshotExtension || '').length),
16+
17+
testPathForConsistencyCheck: 'foo/__tests__/bar.test.js',
18+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"jest": {
3+
"testEnvironment": "node",
4+
"snapshotResolver": "<rootDir>/customSnapshotResolver.mjs"
5+
}
6+
}

packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {TestFileEvent, TestResult} from '@jest/test-result';
1010
import type {Config} from '@jest/types';
1111
import type Runtime from 'jest-runtime';
1212
import type {SnapshotStateType} from 'jest-snapshot';
13-
import {deepCyclicCopy} from 'jest-util';
13+
import {deepCyclicCopy, interopRequireDefault} from 'jest-util';
1414

1515
const FRAMEWORK_INITIALIZER = require.resolve('./jestAdapterInit');
1616

@@ -27,11 +27,28 @@ const jestAdapter = async (
2727
FRAMEWORK_INITIALIZER,
2828
);
2929

30+
const localRequire = async <T = unknown>(
31+
path: string,
32+
applyInteropRequireDefault: boolean = false,
33+
): Promise<T> => {
34+
const esm = runtime.unstable_shouldLoadAsEsm(path);
35+
36+
if (esm) {
37+
return runtime.unstable_importModule(path) as any;
38+
} else {
39+
const requiredModule = runtime.requireModule<T>(path);
40+
if (!applyInteropRequireDefault) {
41+
return requiredModule;
42+
}
43+
return interopRequireDefault(requiredModule).default;
44+
}
45+
};
46+
3047
const {globals, snapshotState} = await initialize({
3148
config,
3249
environment,
3350
globalConfig,
34-
localRequire: runtime.requireModule.bind(runtime),
51+
localRequire,
3552
parentProcess: process,
3653
sendMessageToJest,
3754
setGlobalsForRuntime: runtime.setGlobalsForRuntime.bind(runtime),
@@ -69,21 +86,9 @@ const jestAdapter = async (
6986
});
7087

7188
for (const path of config.setupFilesAfterEnv) {
72-
const esm = runtime.unstable_shouldLoadAsEsm(path);
73-
74-
if (esm) {
75-
await runtime.unstable_importModule(path);
76-
} else {
77-
runtime.requireModule(path);
78-
}
79-
}
80-
const esm = runtime.unstable_shouldLoadAsEsm(testPath);
81-
82-
if (esm) {
83-
await runtime.unstable_importModule(testPath);
84-
} else {
85-
runtime.requireModule(testPath);
89+
await localRequire(path);
8690
}
91+
await localRequire(testPath);
8792

8893
const results = await runAndTransformResultsToJestFormat({
8994
config,

packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ export const initialize = async ({
5555
config: Config.ProjectConfig;
5656
environment: JestEnvironment;
5757
globalConfig: Config.GlobalConfig;
58-
localRequire: <T = unknown>(path: Config.Path) => T;
58+
localRequire: <T = unknown>(
59+
path: Config.Path,
60+
applyInteropRequireDefault?: boolean,
61+
) => Promise<T>;
5962
testPath: Config.Path;
6063
parentProcess: Process;
6164
sendMessageToJest?: TestFileEvent;
@@ -145,10 +148,10 @@ export const initialize = async ({
145148

146149
// Jest tests snapshotSerializers in order preceding built-in serializers.
147150
// Therefore, add in reverse because the last added is the first tested.
148-
config.snapshotSerializers
149-
.concat()
150-
.reverse()
151-
.forEach(path => addSerializer(localRequire(path)));
151+
const snapshotSerializers = config.snapshotSerializers.concat().reverse();
152+
for (const path of snapshotSerializers) {
153+
addSerializer(await localRequire(path));
154+
}
152155

153156
const {expand, updateSnapshot} = globalConfig;
154157
const snapshotResolver = await buildSnapshotResolver(config, localRequire);

packages/jest-jasmine2/src/index.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {AssertionResult, TestResult} from '@jest/test-result';
1212
import type {Config, Global} from '@jest/types';
1313
import type Runtime from 'jest-runtime';
1414
import type {SnapshotStateType} from 'jest-snapshot';
15+
import {interopRequireDefault} from 'jest-util';
1516
import installEach from './each';
1617
import {installErrorOnPrivate} from './errorOnPrivate';
1718
import type Spec from './jasmine/Spec';
@@ -138,14 +139,31 @@ export default async function jasmine2(
138139
});
139140
}
140141

142+
const localRequire = async <T = unknown>(
143+
path: string,
144+
applyInteropRequireDefault: boolean = false,
145+
): Promise<T> => {
146+
const esm = runtime.unstable_shouldLoadAsEsm(path);
147+
148+
if (esm) {
149+
return runtime.unstable_importModule(path) as any;
150+
} else {
151+
const requiredModule = runtime.requireModule<T>(path);
152+
if (!applyInteropRequireDefault) {
153+
return requiredModule;
154+
}
155+
return interopRequireDefault(requiredModule).default;
156+
}
157+
};
158+
141159
const snapshotState: SnapshotStateType = await runtime
142160
.requireInternalModule<typeof import('./setup_jest_globals')>(
143161
path.resolve(__dirname, './setup_jest_globals.js'),
144162
)
145163
.default({
146164
config,
147165
globalConfig,
148-
localRequire: runtime.requireModule.bind(runtime),
166+
localRequire,
149167
testPath,
150168
});
151169

0 commit comments

Comments
 (0)