diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a0b62f24f55..754f602dd2bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Fixes +- `[babel-plugin-jest-hoist]` Preserve order of hoisted mock nodes within containing block ([#10536](https://github.com/facebook/jest/pull/10536)) - `[jest-config]` Fix bug introduced in watch mode by PR[#10678](https://github.com/facebook/jest/pull/10678/files#r511037803) ([#10692](https://github.com/facebook/jest/pull/10692)) - `[expect]` Stop modifying the sample in `expect.objectContaining()` ([#10711](https://github.com/facebook/jest/pull/10711)) - `[jest-circus, jest-jasmine2]` fix: don't assume `stack` is always a string ([#10697](https://github.com/facebook/jest/pull/10697)) diff --git a/e2e/babel-plugin-jest-hoist/__test_modules__/g.js b/e2e/babel-plugin-jest-hoist/__test_modules__/g.js new file mode 100644 index 000000000000..9f271ead5ed7 --- /dev/null +++ b/e2e/babel-plugin-jest-hoist/__test_modules__/g.js @@ -0,0 +1,8 @@ +/** + * 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 () => 'unmocked'; diff --git a/e2e/babel-plugin-jest-hoist/__tests__/integration.test.js b/e2e/babel-plugin-jest-hoist/__tests__/integration.test.js index 066a60bb89c9..4b534faa9fb2 100644 --- a/e2e/babel-plugin-jest-hoist/__tests__/integration.test.js +++ b/e2e/babel-plugin-jest-hoist/__tests__/integration.test.js @@ -75,7 +75,7 @@ myObject.mock('apple', 27); // Variable names prefixed with `mock` (ignore case) should not throw as out-of-scope const MockMethods = () => {}; -jest.mock('../__test_modules__/f', () => MockMethods); +jest.mock('../__test_modules__/g', () => MockMethods); describe('babel-plugin-jest-hoist', () => { it('does not throw during transform', () => { diff --git a/packages/babel-plugin-jest-hoist/package.json b/packages/babel-plugin-jest-hoist/package.json index 8670cb905ecd..877cdc17cb8c 100644 --- a/packages/babel-plugin-jest-hoist/package.json +++ b/packages/babel-plugin-jest-hoist/package.json @@ -20,7 +20,8 @@ }, "devDependencies": { "@types/babel__template": "^7.0.2", - "@types/node": "*" + "@types/node": "*", + "babel-plugin-tester": "9.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap b/packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap new file mode 100644 index 000000000000..0c1a8424ff4f --- /dev/null +++ b/packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`babel-plugin-jest-hoist top level mocking: top level mocking 1`] = ` + +require('x'); + +jest.enableAutomock(); +jest.disableAutomock(); + + ↓ ↓ ↓ ↓ ↓ ↓ + +_getJestObj().enableAutomock(); + +_getJestObj().disableAutomock(); + +function _getJestObj() { + const { jest } = require("@jest/globals"); + + _getJestObj = () => jest; + + return jest; +} + +require("x"); + + +`; + +exports[`babel-plugin-jest-hoist within a block with no siblings: within a block with no siblings 1`] = ` + +beforeEach(() => { + jest.mock('someNode') +}) + + ↓ ↓ ↓ ↓ ↓ ↓ + +function _getJestObj() { + const { jest } = require("@jest/globals"); + + _getJestObj = () => jest; + + return jest; +} + +beforeEach(() => { + _getJestObj().mock("someNode"); +}); + + +`; + +exports[`babel-plugin-jest-hoist within a block: within a block 1`] = ` + +beforeEach(() => { + require('x') + jest.mock('someNode') +}) + + ↓ ↓ ↓ ↓ ↓ ↓ + +function _getJestObj() { + const { jest } = require("@jest/globals"); + + _getJestObj = () => jest; + + return jest; +} + +beforeEach(() => { + _getJestObj().mock("someNode"); + + require("x"); +}); + + +`; diff --git a/packages/babel-plugin-jest-hoist/src/__tests__/hoistPlugin.test.ts b/packages/babel-plugin-jest-hoist/src/__tests__/hoistPlugin.test.ts new file mode 100644 index 000000000000..1e0554a2958c --- /dev/null +++ b/packages/babel-plugin-jest-hoist/src/__tests__/hoistPlugin.test.ts @@ -0,0 +1,43 @@ +/** + * 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 pluginTester from 'babel-plugin-tester'; +import babelPluginJestHoist from '..'; + +pluginTester({ + plugin: babelPluginJestHoist, + pluginName: 'babel-plugin-jest-hoist', + tests: { + 'top level mocking': { + code: ` + require('x'); + + jest.enableAutomock(); + jest.disableAutomock(); + `, + snapshot: true, + }, + 'within a block': { + code: ` + beforeEach(() => { + require('x') + jest.mock('someNode') + }) + `, + snapshot: true, + }, + 'within a block with no siblings': { + code: ` + beforeEach(() => { + jest.mock('someNode') + }) + `, + snapshot: true, + }, + }, +}); diff --git a/packages/babel-plugin-jest-hoist/src/index.ts b/packages/babel-plugin-jest-hoist/src/index.ts index 8fbab87dc0f4..15e2c5bf5ae7 100644 --- a/packages/babel-plugin-jest-hoist/src/index.ts +++ b/packages/babel-plugin-jest-hoist/src/index.ts @@ -8,11 +8,14 @@ import type {NodePath} from '@babel/traverse'; import { + BlockStatement, + CallExpression, Expression, Identifier, Node, Program, callExpression, + emptyStatement, isIdentifier, } from '@babel/types'; import {statement} from '@babel/template'; @@ -113,7 +116,7 @@ FUNCTIONS.mock = args => { const ids: Set> = new Set(); const parentScope = moduleFactory.parentPath.scope; - // @ts-expect-error: ReferencedIdentifier is not known on visitors + // @ts-expect-error: ReferencedIdentifier and blacklist are not known on visitors moduleFactory.traverse(IDVisitor, {ids}); for (const id of ids) { const {name} = id.node; @@ -281,14 +284,33 @@ export default (): PluginObj<{ }, // in `post` to make sure we come after an import transform and can unshift above the `require`s post({path: program}: {path: NodePath}) { + const self = this; + visitBlock(program); program.traverse({ - CallExpression: callExpr => { + BlockStatement: visitBlock, + }); + + function visitBlock(block: NodePath | NodePath) { + // use a temporary empty statement instead of the real first statement, which may itself be hoisted + const [firstNonHoistedStatementOfBlock] = block.unshiftContainer( + 'body', + emptyStatement(), + ); + block.traverse({ + CallExpression: visitCallExpr, + // do not traverse into nested blocks, or we'll hoist calls in there out to this block + // @ts-expect-error blacklist is not known + blacklist: ['BlockStatement'], + }); + firstNonHoistedStatementOfBlock.remove(); + + function visitCallExpr(callExpr: NodePath) { const { node: {callee}, } = callExpr; if ( isIdentifier(callee) && - callee.name === this.jestObjGetterIdentifier?.name + callee.name === self.jestObjGetterIdentifier?.name ) { const mockStmt = callExpr.getStatementParent(); @@ -297,12 +319,12 @@ export default (): PluginObj<{ const mockStmtParent = mockStmt.parentPath; if (mockStmtParent.isBlock()) { mockStmt.remove(); - mockStmtParent.unshiftContainer('body', [mockStmtNode]); + firstNonHoistedStatementOfBlock.insertBefore(mockStmtNode); } } } - }, - }); + } + } }, }); /* eslint-enable */ diff --git a/yarn.lock b/yarn.lock index 88e1380fe069..1a2434ba4635 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3251,6 +3251,16 @@ __metadata: languageName: node linkType: hard +"@types/babel-plugin-tester@npm:^9.0.0": + version: 9.0.0 + resolution: "@types/babel-plugin-tester@npm:9.0.0" + dependencies: + "@types/babel__core": "*" + "@types/prettier": "*" + checksum: d6b465ef6ce980927dd3b10ae0a39fd8384688ba2d15bf2a30ac4ae567572abb2768992e7c2389f873c87c8d9ba5eafe88feb399e1e9e93f3cbfc5fe27da7077 + languageName: node + linkType: hard + "@types/babel-types@npm:*": version: 7.0.9 resolution: "@types/babel-types@npm:7.0.9" @@ -3265,7 +3275,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.0.4, @types/babel__core@npm:^7.1.0, @types/babel__core@npm:^7.1.7": +"@types/babel__core@npm:*, @types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.0.4, @types/babel__core@npm:^7.1.0, @types/babel__core@npm:^7.1.7": version: 7.1.7 resolution: "@types/babel__core@npm:7.1.7" dependencies: @@ -3638,6 +3648,13 @@ __metadata: languageName: node linkType: hard +"@types/prettier@npm:*": + version: 2.1.1 + resolution: "@types/prettier@npm:2.1.1" + checksum: 3671bedc845a0e61bb8eb698746e1f6d1201ac784f95c536cd653c1406a51c0e9c338ecbbc73f1b5fd5fe0b0af98edf7e85428810357d959355ab46b3a63ebe6 + languageName: node + linkType: hard + "@types/prettier@npm:^2.0.0": version: 2.1.5 resolution: "@types/prettier@npm:2.1.5" @@ -4813,6 +4830,7 @@ __metadata: "@types/babel__template": ^7.0.2 "@types/babel__traverse": ^7.0.6 "@types/node": "*" + babel-plugin-tester: 9.2.0 languageName: unknown linkType: soft @@ -4830,6 +4848,20 @@ __metadata: languageName: node linkType: hard +"babel-plugin-tester@npm:9.2.0": + version: 9.2.0 + resolution: "babel-plugin-tester@npm:9.2.0" + dependencies: + "@types/babel-plugin-tester": ^9.0.0 + lodash.mergewith: ^4.6.2 + prettier: ^2.0.1 + strip-indent: ^3.0.0 + peerDependencies: + "@babel/core": ^7.9.0 + checksum: ce247d30010fc4e7f28733187a6b347f292226078a72c58c399d2f30f5ec2a054cdfe81831d3455292a58ab3db3df35cae1db8a44234a35466090b90075a619c + languageName: node + linkType: hard + "babel-plugin-transform-typescript-metadata@npm:*": version: 0.3.1 resolution: "babel-plugin-transform-typescript-metadata@npm:0.3.1" @@ -12841,6 +12873,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"lodash.mergewith@npm:^4.6.2": + version: 4.6.2 + resolution: "lodash.mergewith@npm:4.6.2" + checksum: 3561b63cebc629721ab4c016627fc54929ee33cdef1854b4a15ade71dd8eb5f2fc602830efe5395aed41c607d65e2cce356667116aa7156b82468594b42ab95f + languageName: node + linkType: hard + "lodash.padstart@npm:^4.6.1": version: 4.6.1 resolution: "lodash.padstart@npm:4.6.1" @@ -15865,7 +15904,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"prettier@npm:^2.1.1": +"prettier@npm:^2.0.1, prettier@npm:^2.1.1": version: 2.1.2 resolution: "prettier@npm:2.1.2" bin: