Skip to content

Commit 0e5be15

Browse files
authored
feat(transformer): support hoisting when using @jest/globals (#1937)
Support hoisting when using `@jest/globals`, applying to: - named import, e.g. `import { jest } from '@jest/globals'` - aliased named import, e.g. `import {jest as aliasedJest} from '@jest/globals'` - namespace import, e.g `import * as JestGlobals from '@jest/globals'` Closes #1593
1 parent 77b99c1 commit 0e5be15

File tree

10 files changed

+243
-74
lines changed

10 files changed

+243
-74
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export = 'banana'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
jest.mock('./banana', () => {
2+
const exports = 'apple'
3+
return exports
4+
})

e2e/__cases__/hoisting/general-hoisting.spec.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import b from './__test_modules__/b'
44
import c from './__test_modules__/c'
55
import d from './__test_modules__/d'
66

7+
// The virtual mock call below will be hoisted above this `require` call.
8+
const virtualModule = require('virtual-module')
9+
710
// These will all be hoisted above imports
811
jest.deepUnmock('./__test_modules__/Unmocked')
912
jest.unmock('./__test_modules__/c').unmock('./__test_modules__/d')
@@ -14,13 +17,23 @@ let e: any;
1417
e = require('./__test_modules__/e').default;
1518
// hoisted to the top of the function scope
1619
jest.unmock('./__test_modules__/e')
17-
})();
20+
})()
21+
22+
jest.mock('virtual-module', () => 'kiwi', {virtual: true})
1823

1924
// These will not be hoisted
2025
jest.unmock('./__test_modules__/a').dontMock('./__test_modules__/b')
2126
jest.unmock('./__test_modules__/' + 'a')
2227
jest.dontMock('./__test_modules__/Mocked')
2328

29+
it('does not throw during transform', () => {
30+
const object = {};
31+
// @ts-expect-error
32+
object.__defineGetter__('foo', () => 'bar');
33+
// @ts-expect-error
34+
expect(object.foo).toEqual('bar');
35+
})
36+
2437
it('hoists unmocked modules before imports', () => {
2538
// @ts-expect-error
2639
expect(Unmocked._isMockFunction).toBeUndefined()
@@ -34,16 +47,26 @@ it('hoists unmocked modules before imports', () => {
3447
expect(d._isMockFunction).toBeUndefined()
3548
expect(d()).toEqual('unmocked')
3649

37-
expect(e._isMock).toBe(undefined)
50+
expect(e._isMock).toBeUndefined()
3851
expect(e()).toEqual('unmocked')
39-
});
52+
})
4053

4154
it('does not hoist dontMock calls before imports', () => {
4255
// @ts-expect-error
4356
expect(Mocked._isMockFunction).toBe(true)
44-
expect(new Mocked().isMocked).toEqual(undefined)
57+
expect(new Mocked().isMocked).toBeUndefined()
4558

4659
// @ts-expect-error
4760
expect(b._isMockFunction).toBe(true)
48-
expect(b()).toEqual(undefined)
49-
});
61+
expect(b()).toBeUndefined()
62+
})
63+
64+
it('requires modules that also call jest.mock', () => {
65+
require('./__test_modules__/mockFile')
66+
const mock = require('./__test_modules__/banana')
67+
expect(mock).toEqual('apple')
68+
})
69+
70+
it('works with virtual modules', () => {
71+
expect(virtualModule).toBe('kiwi')
72+
})

e2e/__cases__/hoisting/import-jest.spec.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import {jest} from '@jest/globals'
22
import {jest as aliasedJest} from '@jest/globals'
33
import * as JestGlobals from '@jest/globals'
44

5-
import a from './__test_modules__/a';
6-
import b from './__test_modules__/b';
7-
import c from './__test_modules__/c';
5+
import a from './__test_modules__/a'
6+
import b from './__test_modules__/b'
7+
import c from './__test_modules__/c'
88

99
// These will be hoisted above imports
1010
jest.unmock('./__test_modules__/a')
@@ -17,21 +17,17 @@ test('named import', () => {
1717
// @ts-expect-error
1818
expect(a._isMockFunction).toBeUndefined()
1919
expect(a()).toBe('unmocked')
20-
});
20+
})
2121

2222
test('aliased named import', () => {
23-
// @ts-expect-error TODO: fix aliased named import
24-
expect(b._isMockFunction).toBe(true)
25-
expect(b()).toBeUndefined()
26-
// expect(b._isMockFunction).toBe(undefined)
27-
// expect(b()).toBe('unmocked')
28-
});
23+
// @ts-expect-error
24+
expect(b._isMockFunction).toBeUndefined()
25+
expect(b()).toBe('unmocked')
26+
})
2927

3028
test('namespace import', () => {
31-
// @ts-expect-error TODO: fix namespace import
32-
expect(c._isMockFunction).toBe(true)
33-
expect(c()).toBeUndefined()
34-
// expect(c._isMockFunction).toBe(undefined)
35-
// expect(c()).toBe('unmocked')
36-
});
29+
// @ts-expect-error
30+
expect(c._isMockFunction).toBeUndefined()
31+
expect(c()).toBe('unmocked')
32+
})
3733

e2e/__tests__/__snapshots__/hoisting.test.ts.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ exports[`Hoisting should pass using template "default": should pass using templa
1010
PASS ./import-jest.spec.ts
1111
1212
Test Suites: 2 passed, 2 total
13-
Tests: 5 passed, 5 total
13+
Tests: 8 passed, 8 total
1414
Snapshots: 0 total
1515
Time: XXs
1616
Ran all test suites.
@@ -27,7 +27,7 @@ exports[`Hoisting should pass using template "with-babel-7": should pass using t
2727
PASS ./import-jest.spec.ts
2828
2929
Test Suites: 2 passed, 2 total
30-
Tests: 5 passed, 5 total
30+
Tests: 8 passed, 8 total
3131
Snapshots: 0 total
3232
Time: XXs
3333
Ran all test suites.
@@ -44,7 +44,7 @@ exports[`Hoisting should pass using template "with-babel-7-string-config": shoul
4444
PASS ./import-jest.spec.ts
4545
4646
Test Suites: 2 passed, 2 total
47-
Tests: 5 passed, 5 total
47+
Tests: 8 passed, 8 total
4848
Snapshots: 0 total
4949
Time: XXs
5050
Ran all test suites.

e2e/__tests__/hoisting.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ describe('Hoisting', () => {
55
const testCase = configureTestCase('hoisting', {
66
writeIo: true,
77
jestConfig: {
8+
testEnvironment: 'node',
89
automock: true,
910
},
1011
})

src/config/__snapshots__/config-set.spec.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`cacheKey should be a string 1`] = `"{\\"digest\\":\\"a0d51ca854194df8191d0e65c0ca4730f510f332\\",\\"jest\\":{\\"__backported\\":true,\\"globals\\":{}},\\"transformers\\":[\\"hoisting-jest-mock@2\\"],\\"tsJest\\":{\\"compiler\\":\\"typescript\\",\\"diagnostics\\":{\\"ignoreCodes\\":[6059,18002,18003],\\"pretty\\":true,\\"throws\\":true},\\"isolatedModules\\":false,\\"packageJson\\":{\\"kind\\":\\"file\\"},\\"transformers\\":{},\\"tsConfig\\":{\\"kind\\":\\"file\\",\\"value\\":\\"\\"}},\\"tsconfig\\":{\\"options\\":{\\"configFilePath\\":\\"\\",\\"declaration\\":false,\\"inlineSourceMap\\":false,\\"inlineSources\\":true,\\"module\\":1,\\"noEmit\\":false,\\"removeComments\\":false,\\"sourceMap\\":true,\\"target\\":1,\\"types\\":[]},\\"raw\\":{\\"compileOnSave\\":false,\\"compilerOptions\\":{\\"composite\\":true,\\"declaration\\":true,\\"types\\":[]},\\"exclude\\":[\\"foo/**/*\\"],\\"include\\":[\\"bar/**/*\\"]}}}"`;
3+
exports[`cacheKey should be a string 1`] = `"{\\"digest\\":\\"a0d51ca854194df8191d0e65c0ca4730f510f332\\",\\"jest\\":{\\"__backported\\":true,\\"globals\\":{}},\\"transformers\\":[\\"hoisting-jest-mock@3\\"],\\"tsJest\\":{\\"compiler\\":\\"typescript\\",\\"diagnostics\\":{\\"ignoreCodes\\":[6059,18002,18003],\\"pretty\\":true,\\"throws\\":true},\\"isolatedModules\\":false,\\"packageJson\\":{\\"kind\\":\\"file\\"},\\"transformers\\":{},\\"tsConfig\\":{\\"kind\\":\\"file\\",\\"value\\":\\"\\"}},\\"tsconfig\\":{\\"options\\":{\\"configFilePath\\":\\"\\",\\"declaration\\":false,\\"inlineSourceMap\\":false,\\"inlineSources\\":true,\\"module\\":1,\\"noEmit\\":false,\\"removeComments\\":false,\\"sourceMap\\":true,\\"target\\":1,\\"types\\":[]},\\"raw\\":{\\"compileOnSave\\":false,\\"compilerOptions\\":{\\"composite\\":true,\\"declaration\\":true,\\"types\\":[]},\\"exclude\\":[\\"foo/**/*\\"],\\"include\\":[\\"bar/**/*\\"]}}}"`;
44

55
exports[`isTestFile should return a boolean value whether the file matches test pattern 1`] = `true`;
66

@@ -21,7 +21,7 @@ Object {
2121
"name": undefined,
2222
},
2323
"transformers": Array [
24-
"hoisting-jest-mock@2",
24+
"hoisting-jest-mock@3",
2525
],
2626
"tsJest": Object {
2727
"babelConfig": undefined,

src/transformers/__snapshots__/hoist-jest.spec.ts.snap

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,71 @@ exports[`hoisting should hoist correctly jest methods 1`] = `
44
"\\"use strict\\";
55
Object.defineProperty(exports, \\"__esModule\\", { value: true });
66
// These will all be hoisted above imports
7-
jest.deepUnmock('./__test_modules__/Unmocked');
8-
jest.unmock('./__test_modules__/c').unmock('./__test_modules__/d');
9-
jest.unmock('./__test_modules__/' + 'a');
10-
var Unmocked_1 = require(\\"./__test_modules__/Unmocked\\");
11-
var a_1 = require(\\"./__test_modules__/a\\");
12-
var b_1 = require(\\"./__test_modules__/b\\");
13-
var c_1 = require(\\"./__test_modules__/c\\");
14-
var d_1 = require(\\"./__test_modules__/d\\");
7+
jest.unmock('react');
8+
jest.deepUnmock('../__test_modules__/Unmocked');
9+
jest.unmock('../__test_modules__/c').unmock('../__test_modules__/d');
10+
jest.mock('../__test_modules__/f', function () {
11+
if (!global.CALLS) {
12+
global.CALLS = 0;
13+
}
14+
global.CALLS++;
15+
return {
16+
_isMock: true,
17+
fn: function () {
18+
// The \`jest.mock\` transform will allow require, built-ins and globals.
19+
var path = require('path');
20+
var array = new Array(3);
21+
array[0] = path.sep;
22+
return jest.fn(function () { return array; });
23+
},
24+
};
25+
});
26+
jest.mock(\\"../__test_modules__/jestBackticks\\");
27+
jest.mock('virtual-module', function () { return 'kiwi'; }, { virtual: true });
28+
// This has types that should be ignored by the out-of-scope variables check.
29+
jest.mock('has-flow-types', function () { return function (props) { return 3; }; }, {
30+
virtual: true,
31+
});
32+
jest.unmock('../__test_modules__/' + 'a');
33+
jest.mock('../__test_modules__/f', function () { return MockMethods; });
34+
var Unmocked_1 = require(\\"../__test_modules__/Unmocked\\");
35+
var Mocked_1 = require(\\"../__test_modules__/Mocked\\");
36+
var a_1 = require(\\"../__test_modules__/a\\");
37+
var b_1 = require(\\"../__test_modules__/b\\");
38+
var c_1 = require(\\"../__test_modules__/c\\");
39+
var d_1 = require(\\"../__test_modules__/d\\");
40+
var jestBackticks_1 = require(\\"../__test_modules__/jestBackticks\\");
41+
// The virtual mock call below will be hoisted above this \`require\` call.
42+
var virtualModule = require('virtual-module');
43+
var e;
44+
(function () {
45+
// hoisted to the top of the function scope
46+
jest.unmock('../__test_modules__/e');
47+
var _getJestObj = 42;
48+
e = require('../__test_modules__/e').default;
49+
})();
1550
// These will not be hoisted
16-
jest.unmock('./__test_modules__/a').dontMock('./__test_modules__/b');
51+
jest.unmock('../__test_modules__/a').dontMock('../__test_modules__/b');
52+
jest.dontMock('../__test_modules__/Mocked');
53+
{
54+
// Would error (used before initialization) if hoisted to the top of the scope
55+
jest.unmock('../__test_modules__/a');
56+
var jest = { unmock: function () { } };
57+
}
58+
// This must not throw an error
59+
var myObject = { mock: function () { } };
60+
myObject.mock('apple', 27);
61+
// Variable names prefixed with \`mock\` (ignore case) should not throw as out-of-scope
62+
var MockMethods = function () { };
1763
console.log(Unmocked_1.default);
64+
console.log(Mocked_1.default);
1865
console.log(a_1.default);
1966
console.log(b_1.default);
2067
console.log(c_1.default);
2168
console.log(d_1.default);
69+
console.log(e);
70+
console.log(virtualModule);
71+
console.log(jestBackticks_1.default);
2272
"
2373
`;
2474

@@ -30,12 +80,12 @@ var globals_2 = require(\\"@jest/globals\\");
3080
var JestGlobals = require(\\"@jest/globals\\");
3181
// These will be hoisted above imports
3282
globals_1.jest.unmock('../__test_modules__/a');
83+
globals_2.jest.unmock('../__test_modules__/b');
84+
JestGlobals.jest.unmock('../__test_modules__/c');
3385
var a_1 = require(\\"../__test_modules__/a\\");
3486
var b_1 = require(\\"../__test_modules__/b\\");
3587
var c_1 = require(\\"../__test_modules__/c\\");
3688
var d_1 = require(\\"../__test_modules__/d\\");
37-
globals_2.jest.unmock('../__test_modules__/b');
38-
JestGlobals.jest.unmock('../__test_modules__/c');
3989
// These will not be hoisted above imports
4090
{
4191
jest_1.unmock('../__test_modules__/d');

src/transformers/hoist-jest.spec.ts

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,100 @@ import * as tsc from 'typescript'
44
import * as hoist from './hoist-jest'
55

66
const CODE_WITH_HOISTING_NO_JEST_GLOBALS = `
7-
import Unmocked from './__test_modules__/Unmocked'
8-
import a from './__test_modules__/a'
9-
import b from './__test_modules__/b'
10-
import c from './__test_modules__/c'
11-
import d from './__test_modules__/d'
7+
import React from 'react'
8+
import Unmocked from '../__test_modules__/Unmocked'
9+
import Mocked from '../__test_modules__/Mocked'
10+
import a from '../__test_modules__/a'
11+
import b from '../__test_modules__/b'
12+
import c from '../__test_modules__/c'
13+
import d from '../__test_modules__/d'
14+
import f from '../__test_modules__/f'
15+
import jestBackticks from '../__test_modules__/jestBackticks'
16+
17+
// The virtual mock call below will be hoisted above this \`require\` call.
18+
const virtualModule = require('virtual-module')
1219
1320
// These will all be hoisted above imports
14-
jest.deepUnmock('./__test_modules__/Unmocked')
15-
jest.unmock('./__test_modules__/c').unmock('./__test_modules__/d')
21+
jest.unmock('react')
22+
jest.deepUnmock('../__test_modules__/Unmocked')
23+
jest.unmock('../__test_modules__/c').unmock('../__test_modules__/d')
24+
25+
let e;
26+
(function () {
27+
const _getJestObj = 42;
28+
e = require('../__test_modules__/e').default
29+
// hoisted to the top of the function scope
30+
jest.unmock('../__test_modules__/e')
31+
})()
32+
33+
jest.mock('../__test_modules__/f', () => {
34+
if (!global.CALLS) {
35+
global.CALLS = 0
36+
}
37+
global.CALLS++
38+
39+
return {
40+
_isMock: true,
41+
fn: () => {
42+
// The \`jest.mock\` transform will allow require, built-ins and globals.
43+
const path = require('path')
44+
const array = new Array(3)
45+
array[0] = path.sep
46+
return jest.fn(() => array)
47+
},
48+
};
49+
})
50+
jest.mock(\`../__test_modules__/jestBackticks\`)
51+
jest.mock('virtual-module', () => 'kiwi', {virtual: true})
52+
// This has types that should be ignored by the out-of-scope variables check.
53+
jest.mock('has-flow-types', () => (props: {children: mixed}) => 3, {
54+
virtual: true,
55+
})
1656
1757
// These will not be hoisted
18-
jest.unmock('./__test_modules__/a').dontMock('./__test_modules__/b')
19-
jest.unmock('./__test_modules__/' + 'a')
58+
jest.unmock('../__test_modules__/a').dontMock('../__test_modules__/b')
59+
jest.unmock('../__test_modules__/' + 'a')
60+
jest.dontMock('../__test_modules__/Mocked')
61+
{
62+
const jest = {unmock: () => {}};
63+
// Would error (used before initialization) if hoisted to the top of the scope
64+
jest.unmock('../__test_modules__/a')
65+
}
66+
67+
// This must not throw an error
68+
const myObject = {mock: () => {}}
69+
myObject.mock('apple', 27)
70+
71+
// Variable names prefixed with \`mock\` (ignore case) should not throw as out-of-scope
72+
const MockMethods = () => {}
73+
jest.mock('../__test_modules__/f', () => MockMethods)
2074
2175
console.log(Unmocked)
76+
console.log(Mocked)
2277
console.log(a)
2378
console.log(b)
2479
console.log(c)
2580
console.log(d)
81+
console.log(e)
82+
console.log(virtualModule)
83+
console.log(jestBackticks)
2684
`
2785
const CODE_WITH_HOISTING_HAS_JEST_GLOBALS = `
28-
import a from '../__test_modules__/a';
29-
import b from '../__test_modules__/b';
86+
import a from '../__test_modules__/a'
87+
import b from '../__test_modules__/b'
3088
31-
import {jest} from '@jest/globals';
32-
import {jest as aliasedJest} from '@jest/globals';
33-
import * as JestGlobals from '@jest/globals';
89+
import {jest} from '@jest/globals'
90+
import {jest as aliasedJest} from '@jest/globals'
91+
import * as JestGlobals from '@jest/globals'
3492
35-
import c from '../__test_modules__/c';
36-
import d from '../__test_modules__/d';
93+
import c from '../__test_modules__/c'
94+
import d from '../__test_modules__/d'
3795
3896
// These will be hoisted above imports
3997
40-
jest.unmock('../__test_modules__/a');
41-
aliasedJest.unmock('../__test_modules__/b');
42-
JestGlobals.jest.unmock('../__test_modules__/c');
98+
jest.unmock('../__test_modules__/a')
99+
aliasedJest.unmock('../__test_modules__/b')
100+
JestGlobals.jest.unmock('../__test_modules__/c')
43101
44102
// These will not be hoisted above imports
45103
@@ -66,7 +124,6 @@ describe('hoisting', () => {
66124
expect(typeof hoist.factory).toBe('function')
67125
})
68126

69-
// TODO: import alias and import * are not hoisted correctly yet, will need to fix
70127
it.each([CODE_WITH_HOISTING_NO_JEST_GLOBALS, CODE_WITH_HOISTING_HAS_JEST_GLOBALS])(
71128
'should hoist correctly jest methods',
72129
(data) => {

0 commit comments

Comments
 (0)