Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(snapshot): support snapshotResolver and snapshotSerializers written in ESM #12014

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,8 @@

- `[jest-core]` Add support for `testResultsProcessor` written in ESM ([#12006](https://github.com/facebook/jest/pull/12006))
- `[jest-diff, pretty-format]` Add `compareKeys` option for custom sorting of object keys ([#11992](https://github.com/facebook/jest/pull/11992))
- `[jest-circus, jest-jasmine2, jest-runtime, jest-snapshot]` Add support for `snapshotResolver` and `snapshotSerializers` written in ESM ([#12014](https://github.com/facebook/jest/pull/12014))


### Fixes

Expand Down
38 changes: 38 additions & 0 deletions e2e/__tests__/transform.test.ts
Expand Up @@ -347,4 +347,42 @@ onNodeVersions('>=12.17.0', () => {
expect(json.numPassedTests).toBe(1);
});
});

describe('transform-esm-snapshotResolver', () => {
const dir = path.resolve(
__dirname,
'..',
'transform/transform-esm-snapshotResolver',
);
const snapshotDir = path.resolve(dir, '__snapshots__');
const snapshotFile = path.resolve(snapshotDir, 'snapshot.test.js.snap');

const cleanupTest = () => {
if (fs.existsSync(snapshotFile)) {
fs.unlinkSync(snapshotFile);
}
if (fs.existsSync(snapshotDir)) {
fs.rmdirSync(snapshotDir);
}
};

beforeAll(() => {
runYarnInstall(dir);
});
beforeEach(cleanupTest);
afterAll(cleanupTest);

it('should transform the snapshotResolver', () => {
const result = runJest(dir, ['-w=1', '--no-cache', '--ci=false'], {
nodeOptions: '--experimental-vm-modules --no-warnings',
});

expect(result.stderr).toMatch('1 snapshot written from 1 test suite');

const contents = require(snapshotFile);
expect(contents).toHaveProperty(
'snapshots are written to custom location 1',
);
});
});
});
10 changes: 10 additions & 0 deletions e2e/snapshot-resolver-esm/__tests__/snapshot.test.js
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

test('snapshots are written to custom location', () => {
expect('foobar').toMatchSnapshot();
});
18 changes: 18 additions & 0 deletions e2e/snapshot-resolver-esm/customSnapshotResolver.mjs
@@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

export default {
resolveSnapshotPath: (testPath, snapshotExtension) =>
testPath.replace('__tests__', '__snapshots__') + snapshotExtension,

resolveTestPath: (snapshotFilePath, snapshotExtension) =>
snapshotFilePath
.replace('__snapshots__', '__tests__')
.slice(0, -snapshotExtension.length),

testPathForConsistencyCheck: 'foo/__tests__/bar.test.js',
};
6 changes: 6 additions & 0 deletions e2e/snapshot-resolver-esm/package.json
@@ -0,0 +1,6 @@
{
"jest": {
"testEnvironment": "node",
"snapshotResolver": "<rootDir>/customSnapshotResolver.mjs"
}
}
110 changes: 110 additions & 0 deletions e2e/snapshot-serializers-esm/__tests__/snapshot.test.js
@@ -0,0 +1,110 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use strict';

describe('snapshot serializers', () => {
it('works with first plugin', () => {
const test = {
foo: 1,
};
expect(test).toMatchSnapshot();
});

it('works with second plugin', () => {
const test = {
bar: 2,
};
expect(test).toMatchSnapshot();
});

it('works with nested serializable objects', () => {
const test = {
foo: {
bar: 2,
},
};
expect(test).toMatchSnapshot();
});

it('works with default serializers', () => {
const test = {
$$typeof: Symbol.for('react.test.json'),
children: null,
props: {
id: 'foo',
},
type: 'div',
};
expect(test).toMatchSnapshot();
});

it('works with prepended plugins and default serializers', () => {
const test = {
$$typeof: Symbol.for('react.test.json'),
children: null,
props: {
aProp: {a: 6},
bProp: {foo: 8},
},
type: 'div',
};
expect(test).toMatchSnapshot();
});

it('works with prepended plugins from expect method called once', () => {
const test = {
$$typeof: Symbol.for('react.test.json'),
children: null,
props: {
aProp: {a: 6},
bProp: {foo: 8},
},
type: 'div',
};
// Add plugin that overrides foo specified by Jest config in package.json
expect.addSnapshotSerializer({
print: (val, serialize) => `Foo: ${serialize(val.foo)}`,
test: val => val && val.hasOwnProperty('foo'),
});
expect(test).toMatchSnapshot();
});

it('works with prepended plugins from expect method called twice', () => {
const test = {
$$typeof: Symbol.for('react.test.json'),
children: null,
props: {
aProp: {a: 6},
bProp: {foo: 8},
},
type: 'div',
};
// Add plugin that overrides preceding added plugin
expect.addSnapshotSerializer({
print: (val, serialize) => `FOO: ${serialize(val.foo)}`,
test: val => val && val.hasOwnProperty('foo'),
});
expect(test).toMatchSnapshot();
});

it('works with array of strings in property matcher', () => {
expect({
arrayOfStrings: ['stream'],
}).toMatchSnapshot({
arrayOfStrings: ['stream'],
});
});

it('works with expect.XXX within array in property matcher', () => {
expect({
arrayOfStrings: ['stream'],
}).toMatchSnapshot({
arrayOfStrings: [expect.any(String)],
});
});
});
12 changes: 12 additions & 0 deletions e2e/snapshot-serializers-esm/package.json
@@ -0,0 +1,12 @@
{
"jest": {
"testEnvironment": "node",
"transform": {
"\\.js$": "<rootDir>/transformer.js"
},
"snapshotSerializers": [
"./plugins/foo",
"<rootDir>/plugins/bar"
]
}
}
11 changes: 11 additions & 0 deletions e2e/snapshot-serializers-esm/plugins/bar.mjs
@@ -0,0 +1,11 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {createPlugin} from '../utils';

// We inject the call to "createPlugin('bar') through the transformer"
10 changes: 10 additions & 0 deletions e2e/snapshot-serializers-esm/plugins/foo/index.mjs
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {createPlugin} from '../../utils';
export default createPlugin('foo');
17 changes: 17 additions & 0 deletions e2e/snapshot-serializers-esm/transformer.js
@@ -0,0 +1,17 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

module.exports = {
process(src, filename) {
if (/bar.mjs$/.test(filename)) {
return `${src};\nexport default createPlugin('bar');`;
}
return src;
},
};
12 changes: 12 additions & 0 deletions e2e/snapshot-serializers-esm/utils.mjs
@@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

export const createPlugin = prop => ({
print: (val, serialize) => `${prop} - ${serialize(val[prop])}`,
test: val => val && val.hasOwnProperty(prop),
});
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

test('snapshots are written to custom location', () => {
expect('foobar').toMatchSnapshot();
});
@@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

export default {
resolveSnapshotPath: (testPath, snapshotExtension) =>
testPath.replace('__tests__', '__snapshots__') + snapshotExtension,

resolveTestPath: (snapshotFilePath, snapshotExtension) =>
snapshotFilePath
.replace('__snapshots__', '__tests__')
.slice(0, -(snapshotExtension || '').length),

testPathForConsistencyCheck: 'foo/__tests__/bar.test.js',
};
6 changes: 6 additions & 0 deletions e2e/transform/transform-esm-snapshotResolver/package.json
@@ -0,0 +1,6 @@
{
"jest": {
"testEnvironment": "node",
"snapshotResolver": "<rootDir>/customSnapshotResolver.mjs"
}
}
37 changes: 21 additions & 16 deletions packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts
Expand Up @@ -10,7 +10,7 @@ import type {TestFileEvent, TestResult} from '@jest/test-result';
import type {Config} from '@jest/types';
import type Runtime from 'jest-runtime';
import type {SnapshotStateType} from 'jest-snapshot';
import {deepCyclicCopy} from 'jest-util';
import {deepCyclicCopy, interopRequireDefault} from 'jest-util';

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

Expand All @@ -27,11 +27,28 @@ const jestAdapter = async (
FRAMEWORK_INITIALIZER,
);

const localRequire = async <T = unknown>(
path: string,
applyInteropRequireDefault: boolean = false,
): Promise<T> => {
const esm = runtime.unstable_shouldLoadAsEsm(path);

if (esm) {
return runtime.unstable_importModule(path) as any;
} else {
const requiredModule = runtime.requireModule<T>(path);
if (!applyInteropRequireDefault) {
return requiredModule;
}
return interopRequireDefault(requiredModule).default;
}
};

const {globals, snapshotState} = await initialize({
config,
environment,
globalConfig,
localRequire: runtime.requireModule.bind(runtime),
localRequire,
parentProcess: process,
sendMessageToJest,
setGlobalsForRuntime: runtime.setGlobalsForRuntime.bind(runtime),
Expand Down Expand Up @@ -69,21 +86,9 @@ const jestAdapter = async (
});

for (const path of config.setupFilesAfterEnv) {
const esm = runtime.unstable_shouldLoadAsEsm(path);

if (esm) {
await runtime.unstable_importModule(path);
} else {
runtime.requireModule(path);
}
}
const esm = runtime.unstable_shouldLoadAsEsm(testPath);

if (esm) {
await runtime.unstable_importModule(testPath);
} else {
runtime.requireModule(testPath);
await localRequire(path);
}
await localRequire(testPath);

const results = await runAndTransformResultsToJestFormat({
config,
Expand Down
Expand Up @@ -55,7 +55,10 @@ export const initialize = async ({
config: Config.ProjectConfig;
environment: JestEnvironment;
globalConfig: Config.GlobalConfig;
localRequire: <T = unknown>(path: Config.Path) => T;
localRequire: <T = unknown>(
path: Config.Path,
applyInteropRequireDefault?: boolean,
) => Promise<T>;
testPath: Config.Path;
parentProcess: Process;
sendMessageToJest?: TestFileEvent;
Expand Down Expand Up @@ -145,10 +148,10 @@ export const initialize = async ({

// Jest tests snapshotSerializers in order preceding built-in serializers.
// Therefore, add in reverse because the last added is the first tested.
config.snapshotSerializers
.concat()
.reverse()
.forEach(path => addSerializer(localRequire(path)));
const snapshotSerializers = config.snapshotSerializers.concat().reverse();
for (const path of snapshotSerializers) {
addSerializer(await localRequire(path));
}

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