From a67baaa66ee4b740f8b15e77c0855c4731e1868d Mon Sep 17 00:00:00 2001 From: Caleb Ukle Date: Thu, 9 Mar 2023 15:02:33 -0800 Subject: [PATCH] fix(nx-plugin): run jest 29 migrations for @nrwl/nx-plugin:e2e (#15551) --- packages/nx-plugin/migrations.json | 12 + packages/nx-plugin/package.json | 1 + .../nx-plugin/src/executors/e2e/e2e.impl.ts | 1 + .../update-configs-jest-29.spec.ts.snap | 457 +++++++++++++++++ .../update-tests-jest-29.spec.ts.snap | 193 ++++++++ .../update-15-9-0/jest-29-configs.ts | 230 +++++++++ .../migrations/update-15-9-0/jest-29-tests.ts | 97 ++++ .../update-configs-jest-29.spec.ts | 464 ++++++++++++++++++ .../update-tests-jest-29.spec.ts | 181 +++++++ 9 files changed, 1636 insertions(+) create mode 100644 packages/nx-plugin/src/migrations/update-15-9-0/__snapshots__/update-configs-jest-29.spec.ts.snap create mode 100644 packages/nx-plugin/src/migrations/update-15-9-0/__snapshots__/update-tests-jest-29.spec.ts.snap create mode 100644 packages/nx-plugin/src/migrations/update-15-9-0/jest-29-configs.ts create mode 100644 packages/nx-plugin/src/migrations/update-15-9-0/jest-29-tests.ts create mode 100644 packages/nx-plugin/src/migrations/update-15-9-0/update-configs-jest-29.spec.ts create mode 100644 packages/nx-plugin/src/migrations/update-15-9-0/update-tests-jest-29.spec.ts diff --git a/packages/nx-plugin/migrations.json b/packages/nx-plugin/migrations.json index 7355ea097f..e5ba051377 100644 --- a/packages/nx-plugin/migrations.json +++ b/packages/nx-plugin/migrations.json @@ -17,6 +17,18 @@ "version": "15.0.0-beta.0", "description": "Migrates executor schema files to v2", "factory": "./src/migrations/update-15-0-0/specify-output-capture" + }, + "update-configs-jest-29": { + "version": "15.9.0-beta.0", + "cli": "nx", + "description": "Update nx plugin jest configs to support jest 29 changes (https://jestjs.io/docs/upgrading-to-jest29)", + "factory": "./src/migrations/update-15-9-0/jest-29-configs" + }, + "update-tests-jest-29": { + "version": "15.9.0-beta.0", + "cli": "nx", + "description": "Update nx plugin jest test files to support jest 29 changes (https://jestjs.io/docs/upgrading-to-jest29)", + "factory": "./src/migrations/update-15-9-0/jest-29-tests" } } } diff --git a/packages/nx-plugin/package.json b/packages/nx-plugin/package.json index 33e9202bbb..1a08a70d93 100644 --- a/packages/nx-plugin/package.json +++ b/packages/nx-plugin/package.json @@ -32,6 +32,7 @@ "@nrwl/jest": "file:../jest", "@nrwl/js": "file:../js", "@nrwl/linter": "file:../linter", + "@phenomnomnominal/tsquery": "4.1.1", "dotenv": "~10.0.0", "fs-extra": "^11.1.0", "tslib": "^2.3.0" diff --git a/packages/nx-plugin/src/executors/e2e/e2e.impl.ts b/packages/nx-plugin/src/executors/e2e/e2e.impl.ts index 63e35a8fa0..11ef8b8aa5 100644 --- a/packages/nx-plugin/src/executors/e2e/e2e.impl.ts +++ b/packages/nx-plugin/src/executors/e2e/e2e.impl.ts @@ -12,6 +12,7 @@ import { JestExecutorOptions } from '@nrwl/jest/src/executors/jest/schema'; import { jestExecutor } from '@nrwl/jest/src/executors/jest/jest.impl'; import type { NxPluginE2EExecutorOptions } from './schema'; +// TODO(Caleb & Craigory): can we get rid of this and just use @nrwl/jest directly? export async function* nxPluginE2EExecutor( options: NxPluginE2EExecutorOptions, context: ExecutorContext diff --git a/packages/nx-plugin/src/migrations/update-15-9-0/__snapshots__/update-configs-jest-29.spec.ts.snap b/packages/nx-plugin/src/migrations/update-15-9-0/__snapshots__/update-configs-jest-29.spec.ts.snap new file mode 100644 index 0000000000..96552fabc5 --- /dev/null +++ b/packages/nx-plugin/src/migrations/update-15-9-0/__snapshots__/update-configs-jest-29.spec.ts.snap @@ -0,0 +1,457 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Nx Plugin Migration - jest 29 update configs should NOT update ts-jest with no globals are preset 1`] = ` +"const nxPreset = require('@nrwl/jest/preset').default; +module.exports = { +...nxPreset, +testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'], +transform: { + '^.+\\\\.(ts|js|html)$': 'ts-jest', + }, +resolver: '@nrwl/jest/plugins/resolver', +moduleFileExtensions: ['ts', 'js', 'html'], +coverageReporters: ['html'], +/* TODO: Update to latest Jest snapshotFormat + * By default Nx has kept the older style of Jest Snapshot formats + * to prevent breaking of any existing tests with snapshots. + * It's recommend you update to the latest format. + * You can do this by removing snapshotFormat property + * and running tests with --update-snapshot flag. + * Example: \\"nx affected --targets=test --update-snapshot\\" + * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format + */ +snapshotFormat: { escapeString: true, printBasicPrototype: true } +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should add snapshot config with no root preset 1`] = ` +"/* eslint-disable */ +export default { +displayName: 'my-lib', +preset: '../../jest.preset.js', +globals: { }, +transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json', + }] + }, +moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], +coverageDirectory: '../../coverage/libs/my-lib', +/* TODO: Update to latest Jest snapshotFormat + * By default Nx has kept the older style of Jest Snapshot formats + * to prevent breaking of any existing tests with snapshots. + * It's recommend you update to the latest format. + * You can do this by removing snapshotFormat property + * and running tests with --update-snapshot flag. + * Example: From within the project directory, run \\"nx test --update-snapshot\\" + * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format + */ +snapshotFormat: { escapeString: true, printBasicPrototype: true } +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should add snapshot config with no root preset 2`] = ` +"module.exports = { +transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json' + }] +}, +// I am a comment and shouldn't be removed +moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], +globals: { something: 'else', +abc: [1234, true, {abc: 'yes'}] }, +/** + * Multi-line comment shouldn't be removed + */ +displayName: 'jest', +testEnvironment: 'node', +preset: '../../jest.preset.js', +/* TODO: Update to latest Jest snapshotFormat + * By default Nx has kept the older style of Jest Snapshot formats + * to prevent breaking of any existing tests with snapshots. + * It's recommend you update to the latest format. + * You can do this by removing snapshotFormat property + * and running tests with --update-snapshot flag. + * Example: From within the project directory, run \\"nx test --update-snapshot\\" + * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format + */ +snapshotFormat: { escapeString: true, printBasicPrototype: true } +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should be idempotent 1`] = ` +"/* eslint-disable */ +export default { + displayName: 'my-lib', + preset: '../../jest.preset.js', + globals: { }, + transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json', + }] + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/my-lib' +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should be idempotent 2`] = ` +"module.exports = { +transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json' + }] +}, +// I am a comment and shouldn't be removed +moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], +globals: { something: 'else', +abc: [1234, true, {abc: 'yes'}] }, +/** + * Multi-line comment shouldn't be removed + */ +displayName: 'jest', +testEnvironment: 'node', +preset: '../../jest.preset.js' +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should update globalThis.ngJest.teardown to testEnvironmentOptions 1`] = ` +"globalThis.ngJest = { + +} + +export default { +globals: { }, +transform: { + '^.+.(ts|mjs|js|html)$': ['jest-preset-angular', { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\\\.(html|svg)$', + }], + }, +moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], +displayName: 'jest', +testEnvironment: 'node', +preset: '../../jest.preset.js', +testEnvironmentOptions: { teardown: true }, +};" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should update globalThis.ngJest.teardown to testEnvironmentOptions 2`] = ` +" +globalThis.ngJest = { + ngcc: true, + +} + +module.exports = { + globals: { }, + transform: { + '^.+.(ts|mjs|js|html)$': ['jest-preset-angular', { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\\\.(html|svg)$', + }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + testEnvironmentOptions: { + blah: 123, + teardown: false + }, + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', +};" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should update jest.config.ts 1`] = ` +"/* eslint-disable */ +export default { + displayName: 'my-lib', + preset: '../../jest.preset.js', + globals: { }, + transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json', + }] + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/my-lib' +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should update jest.config.ts 2`] = ` +"module.exports = { +transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json' + }] +}, +// I am a comment and shouldn't be removed +moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], +globals: { something: 'else', +abc: [1234, true, {abc: 'yes'}] }, +/** + * Multi-line comment shouldn't be removed + */ +displayName: 'jest', +testEnvironment: 'node', +preset: '../../jest.preset.js' +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should update root preset 1`] = ` +" + const nxPreset = require('@nrwl/jest/preset').default; + + module.exports = { +...nxPreset, +/* TODO: Update to latest Jest snapshotFormat + * By default Nx has kept the older style of Jest Snapshot formats + * to prevent breaking of any existing tests with snapshots. + * It's recommend you update to the latest format. + * You can do this by removing snapshotFormat property + * and running tests with --update-snapshot flag. + * Example: \\"nx affected --targets=test --update-snapshot\\" + * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format + */ +snapshotFormat: { escapeString: true, printBasicPrototype: true } +}" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should update root preset 2`] = ` +"/* eslint-disable */ +export default { + displayName: 'my-lib', + preset: '../../jest.preset.js', + globals: { }, + transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json', + }] + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/my-lib' +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should update root preset 3`] = ` +"module.exports = { +transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json' + }] +}, +// I am a comment and shouldn't be removed +moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], +globals: { something: 'else', +abc: [1234, true, {abc: 'yes'}] }, +/** + * Multi-line comment shouldn't be removed + */ +displayName: 'jest', +testEnvironment: 'node', +preset: '../../jest.preset.js' +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should update root preset if ts-jest is preset 1`] = ` +"const nxPreset = require('@nrwl/jest/preset').default; +module.exports = { +...nxPreset, +testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'], +globals: { something: 'else', +abc: [1234, true, {abc: 'yes'}] }, +transform: { + '^.+\\\\.(ts|js|html)$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json' + }], + }, +resolver: '@nrwl/jest/plugins/resolver', +moduleFileExtensions: ['ts', 'js', 'html'], +coverageReporters: ['html'], +/* TODO: Update to latest Jest snapshotFormat + * By default Nx has kept the older style of Jest Snapshot formats + * to prevent breaking of any existing tests with snapshots. + * It's recommend you update to the latest format. + * You can do this by removing snapshotFormat property + * and running tests with --update-snapshot flag. + * Example: \\"nx affected --targets=test --update-snapshot\\" + * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format + */ +snapshotFormat: { escapeString: true, printBasicPrototype: true } +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should work if not using ts-jest transformer 1`] = ` +"export default { + transform: { + '^.+\\\\\\\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', +};" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should work if not using ts-jest transformer 2`] = ` +"module.exports = { + transform: { + '^.+\\\\\\\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', +};" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should work snapshotFormat is defined 1`] = ` +"export default { + transform: { + '^.+\\\\\\\\.[tj]sx?$': 'babel-jest', + }, + globals: { something: 'else', +abc: [1234, true, {abc: 'yes'}] }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', + snapshotFormat: {escapeString: false, printBasicPrototype: true} +};" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should work snapshotFormat is defined 2`] = ` +"module.exports = { + transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json' + }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', + globals: { something: 'else', +abc: [1234, true, {abc: 'yes'}] }, + snapshotFormat: {escapeString: false, printBasicPrototype: true} +};" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should work with jest-preset-angular 1`] = ` +"export default { + globals: { }, + transform: { + '^.+.(ts|mjs|js|html)$': ['jest-preset-angular', { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\\\.(html|svg)$', + }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', +};" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should work with jest-preset-angular 2`] = ` +"module.exports = { + globals: { }, + transform: { + '^.+.(ts|mjs|js|html)$': ['jest-preset-angular', { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\\\.(html|svg)$', + }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', +};" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should work with multiple projects + configs 1`] = ` +"/* eslint-disable */ +export default { + displayName: 'my-lib', + preset: '../../jest.preset.js', + globals: { }, + transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json', + }] + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/my-lib' +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should work with multiple projects + configs 2`] = ` +"module.exports = { +transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json' + }] +}, +// I am a comment and shouldn't be removed +moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], +globals: { something: 'else', +abc: [1234, true, {abc: 'yes'}] }, +/** + * Multi-line comment shouldn't be removed + */ +displayName: 'jest', +testEnvironment: 'node', +preset: '../../jest.preset.js' +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should work with multiple projects + configs 3`] = ` +"/* eslint-disable */ +export default { + displayName: 'another-lib', + preset: '../../jest.preset.js', + globals: { }, + transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json', + }] + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/another-lib' +}; +" +`; + +exports[`Nx Plugin Migration - jest 29 update configs should work with multiple projects + configs 4`] = ` +"module.exports = { +transform: { + '^.+\\\\\\\\.[tj]sx?$': ['ts-jest', { + tsconfig: '/tsconfig.spec.json' + }] +}, +// I am a comment and shouldn't be removed +moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], +globals: { something: 'else', +abc: [1234, true, {abc: 'yes'}] }, +/** + * Multi-line comment shouldn't be removed + */ +displayName: 'jest', +testEnvironment: 'node', +preset: '../../jest.preset.js' +}; +" +`; diff --git a/packages/nx-plugin/src/migrations/update-15-9-0/__snapshots__/update-tests-jest-29.spec.ts.snap b/packages/nx-plugin/src/migrations/update-15-9-0/__snapshots__/update-tests-jest-29.spec.ts.snap new file mode 100644 index 0000000000..08fd8ad311 --- /dev/null +++ b/packages/nx-plugin/src/migrations/update-15-9-0/__snapshots__/update-tests-jest-29.spec.ts.snap @@ -0,0 +1,193 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Nx Plugin Migration - jest 29 mocked usage in tests should be idempotent 1`] = ` +" +import{ MaybeMockedDeep, MaybeMocked } from 'jest-mock'; +import {expect, jest, test} from '@jest/globals'; +import {song} from './song'; + +jest.mock('./song'); +jest.spyOn(console, 'log'); + +const mockedSong = jest.mocked(song, true); +// or through \`jest.Mocked\` +// const mockedSong = song as jest.Mocked; + +test('deep method is typed correctly', () => { + mockedSong.one.more.time.mockReturnValue(12); + + expect(mockedSong.one.more.time(10)).toBe(12); + expect(mockedSong.one.more.time.mock.calls).toHaveLength(1); +}); + +test('direct usage', () => { + jest.mocked(console.log).mockImplementation(() => { + return; + }); + + console.log('one more time'); + + expect(jest.mocked(console.log, false).mock.calls).toHaveLength(1); +}); + " +`; + +exports[`Nx Plugin Migration - jest 29 mocked usage in tests should be idempotent 2`] = ` +" +const { MaybeMockedDeep, MaybeMocked } = require('jest-mock'); +const {expect, jest, test} = require('@jest/globals'); +const {song} = require('./song'); + +jest.mock('./song'); +jest.spyOn(console, 'log'); + +const mockedSong = jest.mocked(song, true); +// or through \`jest.Mocked\` +// const mockedSong = song as jest.Mocked; + +test('deep method is typed correctly', () => { + mockedSong.one.more.time.mockReturnValue(12); + + expect(mockedSong.one.more.time(10)).toBe(12); + expect(mockedSong.one.more.time.mock.calls).toHaveLength(1); +}); + +test('direct usage', () => { + jest.mocked(console.log).mockImplementation(() => { + return; + }); + + console.log('one more time'); + + expect(jest.mocked(console.log, false).mock.calls).toHaveLength(1); +}); +" +`; + +exports[`Nx Plugin Migration - jest 29 mocked usage in tests should be idempotent 3`] = ` +" +import{ MaybeMockedDeep, MaybeMocked } from 'jest-mock'; +import {expect, jest, test} from '@jest/globals'; +import {song} from './song'; + +jest.mock('./song'); +jest.spyOn(console, 'log'); + +const mockedSong = jest.mocked(song, true); +// or through \`jest.Mocked\` +// const mockedSong = song as jest.Mocked; + +test('deep method is typed correctly', () => { + mockedSong.one.more.time.mockReturnValue(12); + + expect(mockedSong.one.more.time(10)).toBe(12); + expect(mockedSong.one.more.time.mock.calls).toHaveLength(1); +}); + +test('direct usage', () => { + jest.mocked(console.log).mockImplementation(() => { + return; + }); + + console.log('one more time'); + + expect(jest.mocked(console.log, false).mock.calls).toHaveLength(1); +}); + " +`; + +exports[`Nx Plugin Migration - jest 29 mocked usage in tests should be idempotent 4`] = ` +" +const { MaybeMockedDeep, MaybeMocked } = require('jest-mock'); +const {expect, jest, test} = require('@jest/globals'); +const {song} = require('./song'); + +jest.mock('./song'); +jest.spyOn(console, 'log'); + +const mockedSong = jest.mocked(song, true); +// or through \`jest.Mocked\` +// const mockedSong = song as jest.Mocked; + +test('deep method is typed correctly', () => { + mockedSong.one.more.time.mockReturnValue(12); + + expect(mockedSong.one.more.time(10)).toBe(12); + expect(mockedSong.one.more.time.mock.calls).toHaveLength(1); +}); + +test('direct usage', () => { + jest.mocked(console.log).mockImplementation(() => { + return; + }); + + console.log('one more time'); + + expect(jest.mocked(console.log, false).mock.calls).toHaveLength(1); +}); +" +`; + +exports[`Nx Plugin Migration - jest 29 mocked usage in tests should not update anything if there are no tests 1`] = ` +" +import{ MaybeMockedDeep, MaybeMocked } from 'jest-mock'; +import {expect, jest, test} from '@jest/globals'; +import {song} from './song'; + +jest.mock('./song'); +jest.spyOn(console, 'log'); + +const mockedSong = jest.mocked(song, true); +// or through \`jest.Mocked\` +// const mockedSong = song as jest.Mocked; + +test('deep method is typed correctly', () => { + mockedSong.one.more.time.mockReturnValue(12); + + expect(mockedSong.one.more.time(10)).toBe(12); + expect(mockedSong.one.more.time.mock.calls).toHaveLength(1); +}); + +test('direct usage', () => { + jest.mocked(console.log).mockImplementation(() => { + return; + }); + + console.log('one more time'); + + expect(jest.mocked(console.log, false).mock.calls).toHaveLength(1); +}); + " +`; + +exports[`Nx Plugin Migration - jest 29 mocked usage in tests should not update anything if there are no tests 2`] = ` +" +const { MaybeMockedDeep, MaybeMocked } = require('jest-mock'); +const {expect, jest, test} = require('@jest/globals'); +const {song} = require('./song'); + +jest.mock('./song'); +jest.spyOn(console, 'log'); + +const mockedSong = jest.mocked(song, true); +// or through \`jest.Mocked\` +// const mockedSong = song as jest.Mocked; + +test('deep method is typed correctly', () => { + mockedSong.one.more.time.mockReturnValue(12); + + expect(mockedSong.one.more.time(10)).toBe(12); + expect(mockedSong.one.more.time.mock.calls).toHaveLength(1); +}); + +test('direct usage', () => { + jest.mocked(console.log).mockImplementation(() => { + return; + }); + + console.log('one more time'); + + expect(jest.mocked(console.log, false).mock.calls).toHaveLength(1); +}); +" +`; diff --git a/packages/nx-plugin/src/migrations/update-15-9-0/jest-29-configs.ts b/packages/nx-plugin/src/migrations/update-15-9-0/jest-29-configs.ts new file mode 100644 index 0000000000..20a1c1db44 --- /dev/null +++ b/packages/nx-plugin/src/migrations/update-15-9-0/jest-29-configs.ts @@ -0,0 +1,230 @@ +import { + formatFiles, + logger, + stripIndents, + Tree, + createProjectGraphAsync, +} from '@nrwl/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import * as ts from 'typescript'; +import { forEachExecutorOptionsInGraph } from '@nrwl/workspace/src/utilities/executor-options-utils'; +import { TS_QUERY_JEST_CONFIG_PREFIX } from '@nrwl/jest/src/utils/ast-utils'; +import { findRootJestPreset } from '@nrwl/jest/src/utils/config/find-root-jest-files'; +import { JestExecutorOptions } from '@nrwl/jest/src/executors/jest/schema'; + +// NOTE: this is a copy of the @nrwl/jest v15.8.0 migrations +export async function updateConfigsJest29(tree: Tree) { + const rootPreset = findRootJestPreset(tree); + const targetsWithJest = new Set(); + // have to use graph so the negative configuration targets are expanded + const graph = await createProjectGraphAsync(); + forEachExecutorOptionsInGraph( + graph, + '@nrwl/nx-plugin:e2e', + (options, projectName, targetName) => { + if (options.jestConfig && tree.exists(options.jestConfig)) { + targetsWithJest.add(targetName); + // if the default root preset exists or if the project doesn't have a 'preset' configured + // -> update snapshot config + if (!rootPreset || !hasPresetConfigured(tree, options.jestConfig)) { + addSnapshotOptionsToConfig( + tree, + options.jestConfig, + `From within the project directory, run "nx test --update-snapshot"` + ); + } + updateTsJestOptions(tree, options.jestConfig); + updateNgJestOptions(tree, options.jestConfig); + } + } + ); + + if (rootPreset && tree.exists(rootPreset)) { + const cmd = `"nx affected --targets=${Array.from(targetsWithJest).join( + ',' + )} --update-snapshot"`; + addSnapshotOptionsToConfig(tree, rootPreset, cmd); + updateTsJestOptions(tree, rootPreset); + updateNgJestOptions(tree, rootPreset); + } + + await formatFiles(tree); + logger.info(stripIndents`NX Jest Snapshot format changed in v29. +By default Nx kept the older style to prevent breaking of existing tests with snapshots. +It's recommend you update to the latest format. +You can do this in your project's jest config file. +Remove the snapshotFormat property and re-run tests with the --update-snapshot flag. +More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format`); +} + +function addSnapshotOptionsToConfig( + tree: Tree, + configPath: string, + updateSnapshotExample: string +) { + const config = tree.read(configPath, 'utf-8'); + const hasSnapshotOptions = tsquery.query( + config, + `${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression PropertyAssignment:has(Identifier[name="snapshotFormat"])` + ); + if (hasSnapshotOptions.length > 0) { + return; + } + const updatedConfig = tsquery.replace( + config, + `${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression`, + (node: ts.ObjectLiteralExpression) => { + return `{ +${node.properties.map((p) => getNodeWithComments(config, p)).join(',\n')}, +/* TODO: Update to latest Jest snapshotFormat + * By default Nx has kept the older style of Jest Snapshot formats + * to prevent breaking of any existing tests with snapshots. + * It's recommend you update to the latest format. + * You can do this by removing snapshotFormat property + * and running tests with --update-snapshot flag. + * Example: ${updateSnapshotExample} + * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format + */ +snapshotFormat: { escapeString: true, printBasicPrototype: true } +}`; + }, + { visitAllChildren: false } + ); + + tree.write(configPath, updatedConfig); +} + +function hasPresetConfigured(tree: Tree, configPath: string): boolean { + const contents = tree.read(configPath, 'utf-8'); + + return ( + tsquery.query( + contents, + `${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression PropertyAssignment:has(Identifier[name="preset"])` + )?.length > 0 + ); +} + +function updateTsJestOptions(tree: Tree, configPath: string) { + // query for the globals property, if they don't have one then there's nothing to modify. + const contents = tree.read(configPath, 'utf-8'); + let tsJestGlobalsConfig: string; + const noTsJestGlobals = tsquery.replace( + contents, + `${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression PropertyAssignment:has(Identifier[name="globals"])`, + (node: ts.PropertyAssignment) => { + if (tsJestGlobalsConfig) { + logger.warn( + stripIndents`Found more than one "globals" object in the jest config, ${configPath} + Will use the first one` + ); + return; + } + tsJestGlobalsConfig = getGlobalTsJestConfig(node); + return getGlobalConfigWithoutTsJest(node); + } + ); + + if (!tsJestGlobalsConfig) { + return; + } + + const updatedTsJestTransformer = tsquery.replace( + noTsJestGlobals, + `${TS_QUERY_JEST_CONFIG_PREFIX}> ObjectLiteralExpression PropertyAssignment:has(Identifier[name="transform"]) PropertyAssignment > :has(StringLiteral[value="ts-jest"], StringLiteral[value="jest-preset-angular"])`, + (node: ts.StringLiteral) => { + return `[${node.getText()}, ${tsJestGlobalsConfig}]`; + } + ); + + tree.write(configPath, updatedTsJestTransformer); +} + +function updateNgJestOptions(tree: Tree, configPath: string) { + const contents = tree.read(configPath, 'utf-8'); + + let ngJestTeardownConfig: string; + const noTeardownConfig = tsquery.replace( + contents, + 'BinaryExpression:has(PropertyAccessExpression:has(Identifier[name=ngJest])) PropertyAssignment:has(Identifier[name=teardown])', + (node: ts.PropertyAssignment) => { + ngJestTeardownConfig = node.initializer.getText(); + return ' '; + } + ); + + if (!ngJestTeardownConfig) { + return; + } + + let maybeUpdatedTestEnvOpts = tsquery.replace( + noTeardownConfig, + `${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression PropertyAssignment:has(Identifier[name="testEnvironmentOptions"]) ObjectLiteralExpression`, + (node: ts.ObjectLiteralExpression) => { + return `{ + ${node.properties + .map((p) => getNodeWithComments(noTeardownConfig, p)) + .join(',\n')}, + teardown: ${ngJestTeardownConfig} + }`; + } + ); + + if (maybeUpdatedTestEnvOpts !== noTeardownConfig) { + tree.write(configPath, maybeUpdatedTestEnvOpts); + return; + } + // didn't find existing testEnvironmentOptions, so add the new property + + const updatedConfig = tsquery.replace( + maybeUpdatedTestEnvOpts, + `${TS_QUERY_JEST_CONFIG_PREFIX} > ObjectLiteralExpression`, + (node: ts.ObjectLiteralExpression) => { + return `{ +${node.properties + .map((p) => getNodeWithComments(maybeUpdatedTestEnvOpts, p)) + .join(',\n')}, +testEnvironmentOptions: { teardown: ${ngJestTeardownConfig} }, +}`; + }, + { visitAllChildren: false } + ); + tree.write(configPath, updatedConfig); +} + +function getGlobalTsJestConfig(node: ts.PropertyAssignment): string { + const globalObject = node.initializer as ts.ObjectLiteralExpression; + const foundConfig = globalObject.properties.find( + (p) => ts.isPropertyAssignment(p) && p.name.getText().includes('ts-jest') + ) as ts.PropertyAssignment; + + return foundConfig?.initializer?.getText() || ''; +} + +function getGlobalConfigWithoutTsJest(node: ts.PropertyAssignment): string { + const globalObject = node?.initializer as ts.ObjectLiteralExpression; + const withoutTsJest = globalObject?.properties?.filter((p) => { + return !( + ts.isPropertyAssignment(p) && p.name.getText().includes('ts-jest') + ); + }); + + const globalConfigs = withoutTsJest.map((c) => c.getText()).join(',\n'); + return `globals: { ${globalConfigs} }`; +} + +function getNodeWithComments(fullText: string, node: ts.Node) { + const commentRanges = ts.getLeadingCommentRanges( + fullText, + node.getFullStart() + ); + + if (commentRanges?.length > 0) { + const withComments = `${commentRanges + .map((r) => fullText.slice(r.pos, r.end)) + .join('\n')}\n${node.getText()}`; + return withComments; + } + return node.getText(); +} +export default updateConfigsJest29; diff --git a/packages/nx-plugin/src/migrations/update-15-9-0/jest-29-tests.ts b/packages/nx-plugin/src/migrations/update-15-9-0/jest-29-tests.ts new file mode 100644 index 0000000000..e97e51dc3f --- /dev/null +++ b/packages/nx-plugin/src/migrations/update-15-9-0/jest-29-tests.ts @@ -0,0 +1,97 @@ +import { + createProjectGraphAsync, + formatFiles, + readProjectConfiguration, + Tree, + visitNotIgnoredFiles, +} from '@nrwl/devkit'; +import { JestExecutorOptions } from '@nrwl/jest/src/executors/jest/schema'; +import { TEST_FILE_PATTERN } from '@nrwl/jest/src/utils/ast-utils'; +import { forEachExecutorOptionsInGraph } from '@nrwl/workspace/src/utilities/executor-options-utils'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { + CallExpression, + ImportDeclaration, + VariableStatement, +} from 'typescript'; + +// NOTE: this is a copy of the @nrwl/jest v15.8.0 migrations +export async function updateTestsJest29(tree: Tree) { + const graph = await createProjectGraphAsync(); + forEachExecutorOptionsInGraph( + graph, + '@nrwl/nx-plugin:e2e', + (options, projectName) => { + const projectConfig = readProjectConfiguration(tree, projectName); + visitNotIgnoredFiles( + tree, + projectConfig.sourceRoot || projectConfig.root, + (file) => { + if (!TEST_FILE_PATTERN.test(file)) { + return; + } + updateJestMockTypes(tree, file); + updateJestMocked(tree, file); + } + ); + } + ); + await formatFiles(tree); +} + +export function updateJestMockTypes(tree: Tree, filePath: string) { + const contents = tree.read(filePath, 'utf-8'); + const updatedContent = tsquery.replace( + contents, + ':matches(ImportDeclaration, VariableStatement):has(Identifier[name="MaybeMockedDeep"], Identifier[name="MaybeMocked"]):has(StringLiteral[value="jest-mock"])', + (node: ImportDeclaration | VariableStatement) => { + const text = node.getText(); + return ( + text + // MaybeMockedDeep and MaybeMocked now are exported as Mocked and MockedShallow + .replace('MaybeMockedDeep', 'Mocked') + .replace('MaybeMocked', 'MockedShallow') + ); + } + ); + tree.write(filePath, updatedContent); +} + +export function updateJestMocked(tree: Tree, filePath: string) { + const contents = tree.read(filePath, 'utf-8'); + const jestGlobalNodes = tsquery.query( + contents, + ':matches(ImportDeclaration, VariableStatement):has(Identifier[name="jest"]):has(StringLiteral[value="@jest/globals"])' + ); + + // this only applies if using jest from @jest/globals + if (jestGlobalNodes.length === 0) { + return; + } + + const updatedJestMockTypes = tsquery.replace( + contents, + 'CallExpression:has(Identifier[name="jest"]):has(Identifier[name="mocked"])', + (node: CallExpression) => { + if ( + node.arguments.length === 2 && + node.getText().startsWith('jest.mocked(') + ) { + const text = node.getText(); + // jest.mocked(someObject, true); => jest.mocked(someObject); + if (node.arguments[1].getText() === 'true') { + return text.replace(/,\s*true/g, ''); + } + // jest.mocked(someObject, false); => jest.mocked(someObject, {shallow: true}); + // opt into the new behavior unless explicitly opting out + if (node.arguments[1].getText() === 'false') { + return text.replace('false', '{shallow: true}'); + } + } + } + ); + + tree.write(filePath, updatedJestMockTypes); +} + +export default updateTestsJest29; diff --git a/packages/nx-plugin/src/migrations/update-15-9-0/update-configs-jest-29.spec.ts b/packages/nx-plugin/src/migrations/update-15-9-0/update-configs-jest-29.spec.ts new file mode 100644 index 0000000000..8a73cfac6c --- /dev/null +++ b/packages/nx-plugin/src/migrations/update-15-9-0/update-configs-jest-29.spec.ts @@ -0,0 +1,464 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { + ProjectGraph, + readProjectConfiguration, + Tree, + updateProjectConfiguration, +} from '@nrwl/devkit'; +import { updateConfigsJest29 } from './jest-29-configs'; +import { libraryGenerator } from '@nrwl/workspace'; + +let projectGraph: ProjectGraph; +jest.mock('@nrwl/devkit', () => ({ + ...jest.requireActual('@nrwl/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return projectGraph; + }), +})); + +describe('Nx Plugin Migration - jest 29 update configs', () => { + let tree: Tree; + beforeEach(() => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + }); + afterAll(() => { + jest.resetAllMocks(); + }); + it('should update jest.config.ts', async () => { + await setup(tree, 'my-lib'); + + await updateConfigsJest29(tree); + + const actualJestConfigTs = tree.read('libs/my-lib/jest.config.ts', 'utf-8'); + expect(actualJestConfigTs).toMatchSnapshot(); + const actualJestConfigJs = tree.read('libs/my-lib/jest.config.js', 'utf-8'); + expect(actualJestConfigJs).toMatchSnapshot(); + }); + + it('should update root preset', async () => { + await setup(tree, 'my-lib'); + await updateConfigsJest29(tree); + + const actualPreset = tree.read('jest.preset.js', 'utf-8'); + expect(actualPreset).toMatchSnapshot(); + const actualJestConfigTs = tree.read('libs/my-lib/jest.config.ts', 'utf-8'); + expect(actualJestConfigTs).toMatchSnapshot(); + const actualJestConfigJs = tree.read('libs/my-lib/jest.config.js', 'utf-8'); + expect(actualJestConfigJs).toMatchSnapshot(); + }); + + it('should update root preset if ts-jest is preset', async () => { + await setup(tree, 'my-lib'); + tree.write( + 'jest.preset.js', + `const nxPreset = require('@nrwl/jest/preset').default; +module.exports = { + ...nxPreset, + testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json' + }, + something: 'else', + abc: [1234, true, {abc: 'yes'}] + }, + transform: { + '^.+\\.(ts|js|html)$': 'ts-jest', + }, + resolver: '@nrwl/jest/plugins/resolver', + moduleFileExtensions: ['ts', 'js', 'html'], + coverageReporters: ['html'], +}; +` + ); + + await updateConfigsJest29(tree); + + const actualPreset = tree.read('jest.preset.js', 'utf-8'); + expect(actualPreset).toMatchSnapshot(); + }); + + it('should NOT update ts-jest with no globals are preset', async () => { + await setup(tree, 'my-lib'); + tree.write( + 'jest.preset.js', + `const nxPreset = require('@nrwl/jest/preset').default; +module.exports = { + ...nxPreset, + testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'], + transform: { + '^.+\\.(ts|js|html)$': 'ts-jest', + }, + resolver: '@nrwl/jest/plugins/resolver', + moduleFileExtensions: ['ts', 'js', 'html'], + coverageReporters: ['html'], +}; +` + ); + + await updateConfigsJest29(tree); + + const actualPreset = tree.read('jest.preset.js', 'utf-8'); + expect(actualPreset).toMatchSnapshot(); + }); + + it('should add snapshot config with no root preset', async () => { + await setup(tree, 'my-lib'); + + tree.delete('jest.preset.js'); + + await updateConfigsJest29(tree); + + const actualJestConfigTs = tree.read('libs/my-lib/jest.config.ts', 'utf-8'); + expect(actualJestConfigTs).toMatchSnapshot(); + const actualJestConfigJs = tree.read('libs/my-lib/jest.config.js', 'utf-8'); + expect(actualJestConfigJs).toMatchSnapshot(); + }); + + it('should work with multiple projects + configs', async () => { + await setup(tree, 'my-lib'); + await setup(tree, 'another-lib', projectGraph); + await updateConfigsJest29(tree); + + const actualJestConfigTs1 = tree.read( + 'libs/my-lib/jest.config.ts', + 'utf-8' + ); + expect(actualJestConfigTs1).toMatchSnapshot(); + const actualJestConfigJs1 = tree.read( + 'libs/my-lib/jest.config.js', + 'utf-8' + ); + expect(actualJestConfigJs1).toMatchSnapshot(); + + const actualJestConfigTs2 = tree.read( + 'libs/another-lib/jest.config.ts', + 'utf-8' + ); + expect(actualJestConfigTs2).toMatchSnapshot(); + const actualJestConfigJs2 = tree.read( + 'libs/another-lib/jest.config.js', + 'utf-8' + ); + expect(actualJestConfigJs2).toMatchSnapshot(); + }); + + it('should update globalThis.ngJest.teardown to testEnvironmentOptions ', async () => { + await setup(tree, 'jest-preset-angular'); + tree.write( + `libs/jest-preset-angular/jest.config.ts`, + `globalThis.ngJest = { + teardown: true +} + +export default { + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + } + }, + transform: { + '^.+.(ts|mjs|js|html)$': 'jest-preset-angular', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', +};` + ); + tree.write( + `libs/jest-preset-angular/jest.config.js`, + ` +globalThis.ngJest = { + ngcc: true, + teardown: false +} + +module.exports = { + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + } + }, + transform: { + '^.+.(ts|mjs|js|html)$': 'jest-preset-angular', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + testEnvironmentOptions: { + blah: 123, + }, + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', +};` + ); + await updateConfigsJest29(tree); + const jpaJestConfigTs = tree.read( + `libs/jest-preset-angular/jest.config.ts`, + 'utf-8' + ); + expect(jpaJestConfigTs).toMatchSnapshot(); + const jpaJestConfigJs = tree.read( + `libs/jest-preset-angular/jest.config.js`, + 'utf-8' + ); + expect(jpaJestConfigJs).toMatchSnapshot(); + }); + + it('should work with jest-preset-angular', async () => { + await setup(tree, 'jest-preset-angular'); + tree.write( + `libs/jest-preset-angular/jest.config.ts`, + `export default { + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + } + }, + transform: { + '^.+.(ts|mjs|js|html)$': 'jest-preset-angular', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', +};` + ); + tree.write( + `libs/jest-preset-angular/jest.config.js`, + `module.exports = { + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + } + }, + transform: { + '^.+.(ts|mjs|js|html)$': 'jest-preset-angular', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', +};` + ); + await updateConfigsJest29(tree); + const jpaJestConfigTs = tree.read( + `libs/jest-preset-angular/jest.config.ts`, + 'utf-8' + ); + expect(jpaJestConfigTs).toMatchSnapshot(); + const jpaJestConfigJs = tree.read( + `libs/jest-preset-angular/jest.config.js`, + 'utf-8' + ); + expect(jpaJestConfigJs).toMatchSnapshot(); + }); + + it('should work if not using ts-jest transformer', async () => { + await setup(tree, 'no-ts-jest'); + tree.write( + `libs/no-ts-jest/jest.config.ts`, + `export default { + transform: { + '^.+\\\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', +};` + ); + tree.write( + `libs/no-ts-jest/jest.config.js`, + `module.exports = { + transform: { + '^.+\\\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', +};` + ); + + await updateConfigsJest29(tree); + const noTsJestConfigTs = tree.read( + `libs/no-ts-jest/jest.config.ts`, + 'utf-8' + ); + expect(noTsJestConfigTs).toMatchSnapshot(); + const noTsJestConfigJs = tree.read( + `libs/no-ts-jest/jest.config.js`, + 'utf-8' + ); + expect(noTsJestConfigJs).toMatchSnapshot(); + }); + + it('should work snapshotFormat is defined', async () => { + await setup(tree, 'no-ts-jest'); + tree.write( + `libs/no-ts-jest/jest.config.ts`, + `export default { + transform: { + '^.+\\\\.[tj]sx?$': 'babel-jest', + }, + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json' + }, + something: 'else', + abc: [1234, true, {abc: 'yes'}] + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', + snapshotFormat: {escapeString: false, printBasicPrototype: true} +};` + ); + tree.write( + `libs/no-ts-jest/jest.config.js`, + `module.exports = { + transform: { + '^.+\\\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'] + displayName: 'jest', + testEnvironment: 'node', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json' + }, + something: 'else', + abc: [1234, true, {abc: 'yes'}] + }, + snapshotFormat: {escapeString: false, printBasicPrototype: true} +};` + ); + + await updateConfigsJest29(tree); + const snapshotJestConfigTs = tree.read( + 'libs/no-ts-jest/jest.config.ts', + 'utf-8' + ); + expect(snapshotJestConfigTs).toMatchSnapshot(); + const snapshotJestConfigJs = tree.read( + `libs/no-ts-jest/jest.config.js`, + 'utf-8' + ); + expect(snapshotJestConfigJs).toMatchSnapshot(); + }); + it('should be idempotent', async () => { + await setup(tree, 'my-lib'); + + await updateConfigsJest29(tree); + + const actualJestConfigTs1 = tree.read( + 'libs/my-lib/jest.config.ts', + 'utf-8' + ); + expect(actualJestConfigTs1).toMatchSnapshot(); + const actualJestConfigJs1 = tree.read( + 'libs/my-lib/jest.config.js', + 'utf-8' + ); + expect(actualJestConfigJs1).toMatchSnapshot(); + + await updateConfigsJest29(tree); + + const actualJestConfigTs2 = tree.read( + 'libs/my-lib/jest.config.ts', + 'utf-8' + ); + expect(actualJestConfigTs2).toEqual(actualJestConfigTs1); + const actualJestConfigJs2 = tree.read( + 'libs/my-lib/jest.config.js', + 'utf-8' + ); + expect(actualJestConfigJs2).toEqual(actualJestConfigJs1); + }); +}); + +async function setup(tree: Tree, name: string, existingGraph?: ProjectGraph) { + await libraryGenerator(tree, { + name, + }); + const projectConfig = readProjectConfiguration(tree, name); + projectConfig.targets['test'] = { + ...projectConfig.targets['test'], + executor: '@nrwl/nx-plugin:e2e', + configurations: { + ci: { + ci: true, + }, + other: { + jestConfig: `libs/${name}/jest.config.js`, + }, + }, + }; + + updateProjectConfiguration(tree, name, projectConfig); + tree.write( + `libs/${name}/jest.config.ts`, + `/* eslint-disable */ +export default { + displayName: '${name}', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + } + }, + transform: { + '^.+\\\\.[tj]sx?$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/${name}' +}; +` + ); + + tree.write( + `libs/${name}/jest.config.js`, + `module.exports = { +transform: { + '^.+\\\\.[tj]sx?$': 'ts-jest' +}, +// I am a comment and shouldn't be removed +moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], +globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json' + }, + something: 'else', + abc: [1234, true, {abc: 'yes'}] + }, +/** + * Multi-line comment shouldn't be removed + */ +displayName: 'jest', +testEnvironment: 'node', +preset: '../../jest.preset.js' +}; +` + ); + + projectGraph = { + dependencies: { + ...existingGraph?.dependencies, + }, + nodes: { + ...existingGraph?.nodes, + [name]: { + name, + type: 'lib', + data: projectConfig, + } as any, + }, + }; +} diff --git a/packages/nx-plugin/src/migrations/update-15-9-0/update-tests-jest-29.spec.ts b/packages/nx-plugin/src/migrations/update-15-9-0/update-tests-jest-29.spec.ts new file mode 100644 index 0000000000..63be11c244 --- /dev/null +++ b/packages/nx-plugin/src/migrations/update-15-9-0/update-tests-jest-29.spec.ts @@ -0,0 +1,181 @@ +import { + ProjectGraph, + readProjectConfiguration, + Tree, + updateProjectConfiguration, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { libraryGenerator } from '@nrwl/workspace'; +import { updateTestsJest29 } from './jest-29-tests'; + +let projectGraph: ProjectGraph; +jest.mock('@nrwl/devkit', () => ({ + ...jest.requireActual('@nrwl/devkit'), + createProjectGraphAsync: jest + .fn() + .mockImplementation(async () => projectGraph), +})); +describe('Nx Plugin Migration - jest 29 mocked usage in tests', () => { + let tree: Tree; + beforeEach(() => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + }); + + it('should not update anything if there are no tests', async () => { + await setup(tree, 'my-lib'); + const expected = tree.read('libs/my-lib/src/lib/my-lib.spec.ts', 'utf-8'); + await updateTestsJest29(tree); + expect( + tree.read('libs/my-lib/src/file-one.spec.ts', 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read('libs/my-lib/src/file-two.spec.ts', 'utf-8') + ).toMatchSnapshot(); + expect(tree.read('libs/my-lib/src/lib/my-lib.spec.ts', 'utf-8')).toEqual( + expected + ); + }); + it('should be idempotent', async () => { + await setup(tree, 'my-lib'); + + const expected = tree.read('libs/my-lib/src/lib/my-lib.spec.ts', 'utf-8'); + + await updateTestsJest29(tree); + + expect( + tree.read('libs/my-lib/src/file-one.spec.ts', 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read('libs/my-lib/src/file-two.spec.ts', 'utf-8') + ).toMatchSnapshot(); + expect(tree.read('libs/my-lib/src/lib/my-lib.spec.ts', 'utf-8')).toEqual( + expected + ); + + await updateTestsJest29(tree); + + expect( + tree.read('libs/my-lib/src/file-one.spec.ts', 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read('libs/my-lib/src/file-two.spec.ts', 'utf-8') + ).toMatchSnapshot(); + expect(tree.read('libs/my-lib/src/lib/my-lib.spec.ts', 'utf-8')).toEqual( + expected + ); + }); +}); + +async function setup(tree: Tree, name: string) { + await libraryGenerator(tree, { + name, + }); + const projectConfig = readProjectConfiguration(tree, name); + projectConfig.targets['test'] = { + ...projectConfig.targets['test'], + configurations: { + ci: { + ci: true, + }, + other: { + jestConfig: `libs/${name}/jest.config.js`, + }, + }, + }; + + updateProjectConfiguration(tree, name, projectConfig); + + tree.write( + `libs/${name}/jest.config.js`, + `module.exports = { +transform: { + '^.+\\\\.[tj]sx?$': 'ts-jest' +}, +moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], +globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json' + } + }, +displayName: 'jest', +testEnvironment: 'node', +preset: '../../jest.preset.js' +}; + +` + ); + tree.write( + `libs/${name}/src/file-one.spec.ts`, + ` +import{ MaybeMockedDeep, MaybeMocked } from 'jest-mock'; +import {expect, jest, test} from '@jest/globals'; +import {song} from './song'; + +jest.mock('./song'); +jest.spyOn(console, 'log'); + +const mockedSong = jest.mocked(song, true); +// or through \`jest.Mocked\` +// const mockedSong = song as jest.Mocked; + +test('deep method is typed correctly', () => { + mockedSong.one.more.time.mockReturnValue(12); + + expect(mockedSong.one.more.time(10)).toBe(12); + expect(mockedSong.one.more.time.mock.calls).toHaveLength(1); +}); + +test('direct usage', () => { + jest.mocked(console.log).mockImplementation(() => { + return; + }); + + console.log('one more time'); + + expect(jest.mocked(console.log, false).mock.calls).toHaveLength(1); +}); + ` + ); + tree.write( + `libs/${name}/src/file-two.spec.ts`, + ` +const { MaybeMockedDeep, MaybeMocked } = require('jest-mock'); +const {expect, jest, test} = require('@jest/globals'); +const {song} = require('./song'); + +jest.mock('./song'); +jest.spyOn(console, 'log'); + +const mockedSong = jest.mocked(song, true); +// or through \`jest.Mocked\` +// const mockedSong = song as jest.Mocked; + +test('deep method is typed correctly', () => { + mockedSong.one.more.time.mockReturnValue(12); + + expect(mockedSong.one.more.time(10)).toBe(12); + expect(mockedSong.one.more.time.mock.calls).toHaveLength(1); +}); + +test('direct usage', () => { + jest.mocked(console.log).mockImplementation(() => { + return; + }); + + console.log('one more time'); + + expect(jest.mocked(console.log, false).mock.calls).toHaveLength(1); +}); +` + ); + projectGraph = { + dependencies: {}, + nodes: { + [name]: { + name, + type: 'lib', + data: projectConfig, + } as any, + }, + }; +}