Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ahnpnl committed Sep 10, 2020
1 parent 77b99c1 commit 0e5be15
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 74 deletions.
1 change: 1 addition & 0 deletions e2e/__cases__/hoisting/__test_modules__/banana.ts
@@ -0,0 +1 @@
export = 'banana'
4 changes: 4 additions & 0 deletions e2e/__cases__/hoisting/__test_modules__/mockFile.ts
@@ -0,0 +1,4 @@
jest.mock('./banana', () => {
const exports = 'apple'
return exports
})
35 changes: 29 additions & 6 deletions e2e/__cases__/hoisting/general-hoisting.spec.ts
Expand Up @@ -4,6 +4,9 @@ import b from './__test_modules__/b'
import c from './__test_modules__/c'
import d from './__test_modules__/d'

// The virtual mock call below will be hoisted above this `require` call.
const virtualModule = require('virtual-module')

// These will all be hoisted above imports
jest.deepUnmock('./__test_modules__/Unmocked')
jest.unmock('./__test_modules__/c').unmock('./__test_modules__/d')
Expand All @@ -14,13 +17,23 @@ let e: any;
e = require('./__test_modules__/e').default;
// hoisted to the top of the function scope
jest.unmock('./__test_modules__/e')
})();
})()

jest.mock('virtual-module', () => 'kiwi', {virtual: true})

// These will not be hoisted
jest.unmock('./__test_modules__/a').dontMock('./__test_modules__/b')
jest.unmock('./__test_modules__/' + 'a')
jest.dontMock('./__test_modules__/Mocked')

it('does not throw during transform', () => {
const object = {};
// @ts-expect-error
object.__defineGetter__('foo', () => 'bar');
// @ts-expect-error
expect(object.foo).toEqual('bar');
})

it('hoists unmocked modules before imports', () => {
// @ts-expect-error
expect(Unmocked._isMockFunction).toBeUndefined()
Expand All @@ -34,16 +47,26 @@ it('hoists unmocked modules before imports', () => {
expect(d._isMockFunction).toBeUndefined()
expect(d()).toEqual('unmocked')

expect(e._isMock).toBe(undefined)
expect(e._isMock).toBeUndefined()
expect(e()).toEqual('unmocked')
});
})

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

// @ts-expect-error
expect(b._isMockFunction).toBe(true)
expect(b()).toEqual(undefined)
});
expect(b()).toBeUndefined()
})

it('requires modules that also call jest.mock', () => {
require('./__test_modules__/mockFile')
const mock = require('./__test_modules__/banana')
expect(mock).toEqual('apple')
})

it('works with virtual modules', () => {
expect(virtualModule).toBe('kiwi')
})
28 changes: 12 additions & 16 deletions e2e/__cases__/hoisting/import-jest.spec.ts
Expand Up @@ -2,9 +2,9 @@ import {jest} from '@jest/globals'
import {jest as aliasedJest} from '@jest/globals'
import * as JestGlobals from '@jest/globals'

import a from './__test_modules__/a';
import b from './__test_modules__/b';
import c from './__test_modules__/c';
import a from './__test_modules__/a'
import b from './__test_modules__/b'
import c from './__test_modules__/c'

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

test('aliased named import', () => {
// @ts-expect-error TODO: fix aliased named import
expect(b._isMockFunction).toBe(true)
expect(b()).toBeUndefined()
// expect(b._isMockFunction).toBe(undefined)
// expect(b()).toBe('unmocked')
});
// @ts-expect-error
expect(b._isMockFunction).toBeUndefined()
expect(b()).toBe('unmocked')
})

test('namespace import', () => {
// @ts-expect-error TODO: fix namespace import
expect(c._isMockFunction).toBe(true)
expect(c()).toBeUndefined()
// expect(c._isMockFunction).toBe(undefined)
// expect(c()).toBe('unmocked')
});
// @ts-expect-error
expect(c._isMockFunction).toBeUndefined()
expect(c()).toBe('unmocked')
})

6 changes: 3 additions & 3 deletions e2e/__tests__/__snapshots__/hoisting.test.ts.snap
Expand Up @@ -10,7 +10,7 @@ exports[`Hoisting should pass using template "default": should pass using templa
PASS ./import-jest.spec.ts
Test Suites: 2 passed, 2 total
Tests: 5 passed, 5 total
Tests: 8 passed, 8 total
Snapshots: 0 total
Time: XXs
Ran all test suites.
Expand All @@ -27,7 +27,7 @@ exports[`Hoisting should pass using template "with-babel-7": should pass using t
PASS ./import-jest.spec.ts
Test Suites: 2 passed, 2 total
Tests: 5 passed, 5 total
Tests: 8 passed, 8 total
Snapshots: 0 total
Time: XXs
Ran all test suites.
Expand All @@ -44,7 +44,7 @@ exports[`Hoisting should pass using template "with-babel-7-string-config": shoul
PASS ./import-jest.spec.ts
Test Suites: 2 passed, 2 total
Tests: 5 passed, 5 total
Tests: 8 passed, 8 total
Snapshots: 0 total
Time: XXs
Ran all test suites.
Expand Down
1 change: 1 addition & 0 deletions e2e/__tests__/hoisting.test.ts
Expand Up @@ -5,6 +5,7 @@ describe('Hoisting', () => {
const testCase = configureTestCase('hoisting', {
writeIo: true,
jestConfig: {
testEnvironment: 'node',
automock: true,
},
})
Expand Down
4 changes: 2 additions & 2 deletions src/config/__snapshots__/config-set.spec.ts.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

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/**/*\\"]}}}"`;
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/**/*\\"]}}}"`;

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

Expand All @@ -21,7 +21,7 @@ Object {
"name": undefined,
},
"transformers": Array [
"hoisting-jest-mock@2",
"hoisting-jest-mock@3",
],
"tsJest": Object {
"babelConfig": undefined,
Expand Down
72 changes: 61 additions & 11 deletions src/transformers/__snapshots__/hoist-jest.spec.ts.snap
Expand Up @@ -4,21 +4,71 @@ exports[`hoisting should hoist correctly jest methods 1`] = `
"\\"use strict\\";
Object.defineProperty(exports, \\"__esModule\\", { value: true });
// These will all be hoisted above imports
jest.deepUnmock('./__test_modules__/Unmocked');
jest.unmock('./__test_modules__/c').unmock('./__test_modules__/d');
jest.unmock('./__test_modules__/' + 'a');
var Unmocked_1 = require(\\"./__test_modules__/Unmocked\\");
var a_1 = require(\\"./__test_modules__/a\\");
var b_1 = require(\\"./__test_modules__/b\\");
var c_1 = require(\\"./__test_modules__/c\\");
var d_1 = require(\\"./__test_modules__/d\\");
jest.unmock('react');
jest.deepUnmock('../__test_modules__/Unmocked');
jest.unmock('../__test_modules__/c').unmock('../__test_modules__/d');
jest.mock('../__test_modules__/f', function () {
if (!global.CALLS) {
global.CALLS = 0;
}
global.CALLS++;
return {
_isMock: true,
fn: function () {
// The \`jest.mock\` transform will allow require, built-ins and globals.
var path = require('path');
var array = new Array(3);
array[0] = path.sep;
return jest.fn(function () { return array; });
},
};
});
jest.mock(\\"../__test_modules__/jestBackticks\\");
jest.mock('virtual-module', function () { return 'kiwi'; }, { virtual: true });
// This has types that should be ignored by the out-of-scope variables check.
jest.mock('has-flow-types', function () { return function (props) { return 3; }; }, {
virtual: true,
});
jest.unmock('../__test_modules__/' + 'a');
jest.mock('../__test_modules__/f', function () { return MockMethods; });
var Unmocked_1 = require(\\"../__test_modules__/Unmocked\\");
var Mocked_1 = require(\\"../__test_modules__/Mocked\\");
var a_1 = require(\\"../__test_modules__/a\\");
var b_1 = require(\\"../__test_modules__/b\\");
var c_1 = require(\\"../__test_modules__/c\\");
var d_1 = require(\\"../__test_modules__/d\\");
var jestBackticks_1 = require(\\"../__test_modules__/jestBackticks\\");
// The virtual mock call below will be hoisted above this \`require\` call.
var virtualModule = require('virtual-module');
var e;
(function () {
// hoisted to the top of the function scope
jest.unmock('../__test_modules__/e');
var _getJestObj = 42;
e = require('../__test_modules__/e').default;
})();
// These will not be hoisted
jest.unmock('./__test_modules__/a').dontMock('./__test_modules__/b');
jest.unmock('../__test_modules__/a').dontMock('../__test_modules__/b');
jest.dontMock('../__test_modules__/Mocked');
{
// Would error (used before initialization) if hoisted to the top of the scope
jest.unmock('../__test_modules__/a');
var jest = { unmock: function () { } };
}
// This must not throw an error
var myObject = { mock: function () { } };
myObject.mock('apple', 27);
// Variable names prefixed with \`mock\` (ignore case) should not throw as out-of-scope
var MockMethods = function () { };
console.log(Unmocked_1.default);
console.log(Mocked_1.default);
console.log(a_1.default);
console.log(b_1.default);
console.log(c_1.default);
console.log(d_1.default);
console.log(e);
console.log(virtualModule);
console.log(jestBackticks_1.default);
"
`;

Expand All @@ -30,12 +80,12 @@ var globals_2 = require(\\"@jest/globals\\");
var JestGlobals = require(\\"@jest/globals\\");
// These will be hoisted above imports
globals_1.jest.unmock('../__test_modules__/a');
globals_2.jest.unmock('../__test_modules__/b');
JestGlobals.jest.unmock('../__test_modules__/c');
var a_1 = require(\\"../__test_modules__/a\\");
var b_1 = require(\\"../__test_modules__/b\\");
var c_1 = require(\\"../__test_modules__/c\\");
var d_1 = require(\\"../__test_modules__/d\\");
globals_2.jest.unmock('../__test_modules__/b');
JestGlobals.jest.unmock('../__test_modules__/c');
// These will not be hoisted above imports
{
jest_1.unmock('../__test_modules__/d');
Expand Down
97 changes: 77 additions & 20 deletions src/transformers/hoist-jest.spec.ts
Expand Up @@ -4,42 +4,100 @@ import * as tsc from 'typescript'
import * as hoist from './hoist-jest'

const CODE_WITH_HOISTING_NO_JEST_GLOBALS = `
import Unmocked from './__test_modules__/Unmocked'
import a from './__test_modules__/a'
import b from './__test_modules__/b'
import c from './__test_modules__/c'
import d from './__test_modules__/d'
import React from 'react'
import Unmocked from '../__test_modules__/Unmocked'
import Mocked from '../__test_modules__/Mocked'
import a from '../__test_modules__/a'
import b from '../__test_modules__/b'
import c from '../__test_modules__/c'
import d from '../__test_modules__/d'
import f from '../__test_modules__/f'
import jestBackticks from '../__test_modules__/jestBackticks'
// The virtual mock call below will be hoisted above this \`require\` call.
const virtualModule = require('virtual-module')
// These will all be hoisted above imports
jest.deepUnmock('./__test_modules__/Unmocked')
jest.unmock('./__test_modules__/c').unmock('./__test_modules__/d')
jest.unmock('react')
jest.deepUnmock('../__test_modules__/Unmocked')
jest.unmock('../__test_modules__/c').unmock('../__test_modules__/d')
let e;
(function () {
const _getJestObj = 42;
e = require('../__test_modules__/e').default
// hoisted to the top of the function scope
jest.unmock('../__test_modules__/e')
})()
jest.mock('../__test_modules__/f', () => {
if (!global.CALLS) {
global.CALLS = 0
}
global.CALLS++
return {
_isMock: true,
fn: () => {
// The \`jest.mock\` transform will allow require, built-ins and globals.
const path = require('path')
const array = new Array(3)
array[0] = path.sep
return jest.fn(() => array)
},
};
})
jest.mock(\`../__test_modules__/jestBackticks\`)
jest.mock('virtual-module', () => 'kiwi', {virtual: true})
// This has types that should be ignored by the out-of-scope variables check.
jest.mock('has-flow-types', () => (props: {children: mixed}) => 3, {
virtual: true,
})
// These will not be hoisted
jest.unmock('./__test_modules__/a').dontMock('./__test_modules__/b')
jest.unmock('./__test_modules__/' + 'a')
jest.unmock('../__test_modules__/a').dontMock('../__test_modules__/b')
jest.unmock('../__test_modules__/' + 'a')
jest.dontMock('../__test_modules__/Mocked')
{
const jest = {unmock: () => {}};
// Would error (used before initialization) if hoisted to the top of the scope
jest.unmock('../__test_modules__/a')
}
// This must not throw an error
const myObject = {mock: () => {}}
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)
console.log(Unmocked)
console.log(Mocked)
console.log(a)
console.log(b)
console.log(c)
console.log(d)
console.log(e)
console.log(virtualModule)
console.log(jestBackticks)
`
const CODE_WITH_HOISTING_HAS_JEST_GLOBALS = `
import a from '../__test_modules__/a';
import b from '../__test_modules__/b';
import a from '../__test_modules__/a'
import b from '../__test_modules__/b'
import {jest} from '@jest/globals';
import {jest as aliasedJest} from '@jest/globals';
import * as JestGlobals from '@jest/globals';
import {jest} from '@jest/globals'
import {jest as aliasedJest} from '@jest/globals'
import * as JestGlobals from '@jest/globals'
import c from '../__test_modules__/c';
import d from '../__test_modules__/d';
import c from '../__test_modules__/c'
import d from '../__test_modules__/d'
// These will be hoisted above imports
jest.unmock('../__test_modules__/a');
aliasedJest.unmock('../__test_modules__/b');
JestGlobals.jest.unmock('../__test_modules__/c');
jest.unmock('../__test_modules__/a')
aliasedJest.unmock('../__test_modules__/b')
JestGlobals.jest.unmock('../__test_modules__/c')
// These will not be hoisted above imports
Expand All @@ -66,7 +124,6 @@ describe('hoisting', () => {
expect(typeof hoist.factory).toBe('function')
})

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

0 comments on commit 0e5be15

Please sign in to comment.