From d19c3afd85bd0817bd6455e7c703d537435983ea Mon Sep 17 00:00:00 2001 From: chentsulin Date: Fri, 29 Oct 2021 13:04:40 +0800 Subject: [PATCH] feat(snapshot): support snapshotResolver and snapshotSerializers written in ESM --- e2e/__tests__/circusDeclarationErrors.test.ts | 4 +- e2e/__tests__/transform.test.ts | 38 ++++++ .../__tests__/snapshot.test.js | 110 ++++++++++++++++++ e2e/snapshot-serializers-esm/package.json | 12 ++ e2e/snapshot-serializers-esm/plugins/bar.mjs | 11 ++ .../plugins/foo/index.mjs | 10 ++ e2e/snapshot-serializers-esm/transformer.js | 17 +++ e2e/snapshot-serializers-esm/utils.mjs | 12 ++ .../__tests__/snapshot.test.js | 10 ++ .../customSnapshotResolver.mjs | 18 +++ .../package.json | 6 + .../legacy-code-todo-rewrite/jestAdapter.ts | 37 +++--- .../jestAdapterInit.ts | 13 ++- packages/jest-jasmine2/src/index.ts | 20 +++- .../jest-jasmine2/src/setup_jest_globals.ts | 16 +-- .../jest-snapshot/src/SnapshotResolver.ts | 23 ++-- .../src/__tests__/SnapshotResolver.test.ts | 39 +++++++ .../fixtures/customSnapshotResolver.mjs | 18 +++ 18 files changed, 372 insertions(+), 42 deletions(-) create mode 100644 e2e/snapshot-serializers-esm/__tests__/snapshot.test.js create mode 100644 e2e/snapshot-serializers-esm/package.json create mode 100644 e2e/snapshot-serializers-esm/plugins/bar.mjs create mode 100644 e2e/snapshot-serializers-esm/plugins/foo/index.mjs create mode 100644 e2e/snapshot-serializers-esm/transformer.js create mode 100644 e2e/snapshot-serializers-esm/utils.mjs create mode 100644 e2e/transform/transform-esm-snapshotResolver/__tests__/snapshot.test.js create mode 100644 e2e/transform/transform-esm-snapshotResolver/customSnapshotResolver.mjs create mode 100644 e2e/transform/transform-esm-snapshotResolver/package.json create mode 100644 packages/jest-snapshot/src/__tests__/fixtures/customSnapshotResolver.mjs diff --git a/e2e/__tests__/circusDeclarationErrors.test.ts b/e2e/__tests__/circusDeclarationErrors.test.ts index 667b1742b4f5..fc9763d2f7a0 100644 --- a/e2e/__tests__/circusDeclarationErrors.test.ts +++ b/e2e/__tests__/circusDeclarationErrors.test.ts @@ -12,8 +12,8 @@ import runJest from '../runJest'; skipSuiteOnJasmine(); -it('defining tests and hooks asynchronously throws', () => { - const result = runJest('circus-declaration-errors', [ +it('defining tests and hooks asynchronously throws', async () => { + const result = await runJest('circus-declaration-errors', [ 'asyncDefinition.test.js', ]); diff --git a/e2e/__tests__/transform.test.ts b/e2e/__tests__/transform.test.ts index 5e088066e7d7..b71161411596 100644 --- a/e2e/__tests__/transform.test.ts +++ b/e2e/__tests__/transform.test.ts @@ -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', + ); + }); + }); }); diff --git a/e2e/snapshot-serializers-esm/__tests__/snapshot.test.js b/e2e/snapshot-serializers-esm/__tests__/snapshot.test.js new file mode 100644 index 000000000000..093024192c62 --- /dev/null +++ b/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)], + }); + }); +}); diff --git a/e2e/snapshot-serializers-esm/package.json b/e2e/snapshot-serializers-esm/package.json new file mode 100644 index 000000000000..2eab6d3f2150 --- /dev/null +++ b/e2e/snapshot-serializers-esm/package.json @@ -0,0 +1,12 @@ +{ + "jest": { + "testEnvironment": "node", + "transform": { + "\\.js$": "/transformer.js" + }, + "snapshotSerializers": [ + "./plugins/foo", + "/plugins/bar" + ] + } +} diff --git a/e2e/snapshot-serializers-esm/plugins/bar.mjs b/e2e/snapshot-serializers-esm/plugins/bar.mjs new file mode 100644 index 000000000000..e9c5ec48a9b2 --- /dev/null +++ b/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" diff --git a/e2e/snapshot-serializers-esm/plugins/foo/index.mjs b/e2e/snapshot-serializers-esm/plugins/foo/index.mjs new file mode 100644 index 000000000000..29993c95db73 --- /dev/null +++ b/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'); diff --git a/e2e/snapshot-serializers-esm/transformer.js b/e2e/snapshot-serializers-esm/transformer.js new file mode 100644 index 000000000000..ad239d660c68 --- /dev/null +++ b/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; + }, +}; diff --git a/e2e/snapshot-serializers-esm/utils.mjs b/e2e/snapshot-serializers-esm/utils.mjs new file mode 100644 index 000000000000..95e8a512fe80 --- /dev/null +++ b/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), +}); diff --git a/e2e/transform/transform-esm-snapshotResolver/__tests__/snapshot.test.js b/e2e/transform/transform-esm-snapshotResolver/__tests__/snapshot.test.js new file mode 100644 index 000000000000..d1bc86b0996d --- /dev/null +++ b/e2e/transform/transform-esm-snapshotResolver/__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(); +}); diff --git a/e2e/transform/transform-esm-snapshotResolver/customSnapshotResolver.mjs b/e2e/transform/transform-esm-snapshotResolver/customSnapshotResolver.mjs new file mode 100644 index 000000000000..7ba707ecbe98 --- /dev/null +++ b/e2e/transform/transform-esm-snapshotResolver/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', +}; diff --git a/e2e/transform/transform-esm-snapshotResolver/package.json b/e2e/transform/transform-esm-snapshotResolver/package.json new file mode 100644 index 000000000000..418f68b08ca8 --- /dev/null +++ b/e2e/transform/transform-esm-snapshotResolver/package.json @@ -0,0 +1,6 @@ +{ + "jest": { + "testEnvironment": "node", + "snapshotResolver": "/customSnapshotResolver.mjs" + } +} diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index 3bd1f1554800..9ef9d0e3dc93 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -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'); @@ -27,11 +27,28 @@ const jestAdapter = async ( FRAMEWORK_INITIALIZER, ); + const localRequire = async ( + path: string, + applyInteropRequireDefault: boolean = false, + ): Promise => { + const esm = runtime.unstable_shouldLoadAsEsm(path); + + if (esm) { + return runtime.unstable_importModule(path) as any; + } else { + const requiredModule = runtime.requireModule(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), @@ -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, diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts index d7487477e31d..9bcb6ce4521a 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts @@ -55,7 +55,10 @@ export const initialize = async ({ config: Config.ProjectConfig; environment: JestEnvironment; globalConfig: Config.GlobalConfig; - localRequire: (path: Config.Path) => T; + localRequire: ( + path: Config.Path, + applyInteropRequireDefault?: boolean, + ) => Promise; testPath: Config.Path; parentProcess: Process; sendMessageToJest?: TestFileEvent; @@ -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); diff --git a/packages/jest-jasmine2/src/index.ts b/packages/jest-jasmine2/src/index.ts index efd1b01c30ec..0e8339a54872 100644 --- a/packages/jest-jasmine2/src/index.ts +++ b/packages/jest-jasmine2/src/index.ts @@ -12,6 +12,7 @@ import type {AssertionResult, TestResult} from '@jest/test-result'; import type {Config, Global} from '@jest/types'; import type Runtime from 'jest-runtime'; import type {SnapshotStateType} from 'jest-snapshot'; +import {interopRequireDefault} from 'jest-util'; import installEach from './each'; import {installErrorOnPrivate} from './errorOnPrivate'; import type Spec from './jasmine/Spec'; @@ -138,6 +139,23 @@ export default async function jasmine2( }); } + const localRequire = async ( + path: string, + applyInteropRequireDefault: boolean = false, + ): Promise => { + const esm = runtime.unstable_shouldLoadAsEsm(path); + + if (esm) { + return runtime.unstable_importModule(path) as any; + } else { + const requiredModule = runtime.requireModule(path); + if (!applyInteropRequireDefault) { + return requiredModule; + } + return interopRequireDefault(requiredModule).default; + } + }; + const snapshotState: SnapshotStateType = await runtime .requireInternalModule( path.resolve(__dirname, './setup_jest_globals.js'), @@ -145,7 +163,7 @@ export default async function jasmine2( .default({ config, globalConfig, - localRequire: runtime.requireModule.bind(runtime), + localRequire, testPath, }); diff --git a/packages/jest-jasmine2/src/setup_jest_globals.ts b/packages/jest-jasmine2/src/setup_jest_globals.ts index 7fc6329aa317..60bb14d20379 100644 --- a/packages/jest-jasmine2/src/setup_jest_globals.ts +++ b/packages/jest-jasmine2/src/setup_jest_globals.ts @@ -13,7 +13,6 @@ import { addSerializer, buildSnapshotResolver, } from 'jest-snapshot'; -import type {Plugin} from 'pretty-format'; import type { Attributes, default as JasmineSpec, @@ -26,7 +25,10 @@ declare const global: Global.Global; export type SetupOptions = { config: Config.ProjectConfig; globalConfig: Config.GlobalConfig; - localRequire: (moduleName: string) => Plugin; + localRequire: ( + path: string, + applyInteropRequireDefault?: boolean, + ) => Promise; testPath: Config.Path; }; @@ -97,12 +99,10 @@ export default async ({ }: SetupOptions): Promise => { // 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)); + } patchJasmine(); const {expand, updateSnapshot} = globalConfig; diff --git a/packages/jest-snapshot/src/SnapshotResolver.ts b/packages/jest-snapshot/src/SnapshotResolver.ts index 5e8358e5046d..23880f9e2029 100644 --- a/packages/jest-snapshot/src/SnapshotResolver.ts +++ b/packages/jest-snapshot/src/SnapshotResolver.ts @@ -9,7 +9,6 @@ import * as path from 'path'; import chalk = require('chalk'); import {createTranspilingRequire} from '@jest/transform'; import type {Config} from '@jest/types'; -import {interopRequireDefault} from 'jest-util'; export type SnapshotResolver = { testPathForConsistencyCheck: string; @@ -25,13 +24,16 @@ export const isSnapshotPath = (path: string): boolean => const cache = new Map(); -type LocalRequire = (module: string) => unknown; +type LocalRequire = ( + path: string, + applyInteropRequireDefault?: boolean, +) => Promise; export const buildSnapshotResolver = async ( config: Config.ProjectConfig, - localRequire: Promise | LocalRequire = createTranspilingRequire( - config, - ), + localRequire: + | Promise> + | LocalRequire = createTranspilingRequire(config), ): Promise => { const key = config.rootDir; @@ -45,7 +47,7 @@ export const buildSnapshotResolver = async ( }; async function createSnapshotResolver( - localRequire: LocalRequire, + localRequire: LocalRequire, snapshotResolverPath?: Config.Path | null, ): Promise { return typeof snapshotResolverPath === 'string' @@ -78,11 +80,12 @@ function createDefaultSnapshotResolver(): SnapshotResolver { async function createCustomSnapshotResolver( snapshotResolverPath: Config.Path, - localRequire: LocalRequire, + localRequire: LocalRequire, ): Promise { - const custom: SnapshotResolver = interopRequireDefault( - await localRequire(snapshotResolverPath), - ).default; + const custom: SnapshotResolver = await localRequire( + snapshotResolverPath, + true, + ); const keys: Array<[keyof SnapshotResolver, string]> = [ ['resolveSnapshotPath', 'function'], diff --git a/packages/jest-snapshot/src/__tests__/SnapshotResolver.test.ts b/packages/jest-snapshot/src/__tests__/SnapshotResolver.test.ts index 1fa5786fe5a9..a0455bb49f2e 100644 --- a/packages/jest-snapshot/src/__tests__/SnapshotResolver.test.ts +++ b/packages/jest-snapshot/src/__tests__/SnapshotResolver.test.ts @@ -78,6 +78,45 @@ describe('custom resolver in project config', () => { }); }); +describe('custom resolver written in ESM in project config', () => { + let snapshotResolver: SnapshotResolver; + const customSnapshotResolverFile = path.join( + __dirname, + 'fixtures', + 'customSnapshotResolver.mjs', + ); + const projectConfig = makeProjectConfig({ + rootDir: 'custom1', + snapshotResolver: customSnapshotResolverFile, + }); + + beforeEach(async () => { + snapshotResolver = await buildSnapshotResolver(projectConfig); + }); + + it('returns cached object if called multiple times', async () => { + await expect(buildSnapshotResolver(projectConfig)).resolves.toBe( + snapshotResolver, + ); + }); + + it('resolveSnapshotPath()', () => { + expect( + snapshotResolver.resolveSnapshotPath( + path.resolve('/abc/cde/__tests__/a.test.js'), + ), + ).toBe(path.resolve('/abc/cde/__snapshots__/a.test.js.snap')); + }); + + it('resolveTestPath()', () => { + expect( + snapshotResolver.resolveTestPath( + path.resolve('/abc', 'cde', '__snapshots__', 'a.test.js.snap'), + ), + ).toBe(path.resolve('/abc/cde/__tests__/a.test.js')); + }); +}); + describe('malformed custom resolver in project config', () => { const newProjectConfig = (filename: string) => { const customSnapshotResolverFile = path.join( diff --git a/packages/jest-snapshot/src/__tests__/fixtures/customSnapshotResolver.mjs b/packages/jest-snapshot/src/__tests__/fixtures/customSnapshotResolver.mjs new file mode 100644 index 000000000000..220ed2672784 --- /dev/null +++ b/packages/jest-snapshot/src/__tests__/fixtures/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', +};