Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(transformer): support hoisting when using @jest/globals #1937

Merged
merged 1 commit into from Sep 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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