From 722dc91f923c09491915ec8627abe03018c5a2c9 Mon Sep 17 00:00:00 2001 From: Aaron Abramov Date: Sun, 13 Aug 2017 11:06:05 -0700 Subject: [PATCH] jest.requireActual (#4260) --- .../__tests__/jest_require_actual.test.js | 60 +++++++ .../lib/__tests__/extract_requires.test.js | 162 +++++++++--------- .../src/lib/extract_requires.js | 2 +- packages/jest-runtime/src/index.js | 46 +++-- types/Jest.js | 48 ++++++ 5 files changed, 222 insertions(+), 96 deletions(-) create mode 100644 integration_tests/__tests__/jest_require_actual.test.js create mode 100644 types/Jest.js diff --git a/integration_tests/__tests__/jest_require_actual.test.js b/integration_tests/__tests__/jest_require_actual.test.js new file mode 100644 index 000000000000..ba31d5f208fd --- /dev/null +++ b/integration_tests/__tests__/jest_require_actual.test.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +'use strict'; + +const path = require('path'); +const os = require('os'); +const skipOnWindows = require('../../scripts/skip_on_windows'); +const {cleanup, writeFiles} = require('../utils'); +const runJest = require('../runJest'); + +const DIR = path.resolve(os.tmpdir(), 'jest_require_actual_test'); + +skipOnWindows.suite(); + +beforeEach(() => cleanup(DIR)); +afterAll(() => cleanup(DIR)); + +test('understands dependencies using require.requireActual', () => { + writeFiles(DIR, { + '.watchmanconfig': '', + '__tests__/a.test.js': ` + const a = require.requireActual('../a'); + + test('a', () => {}); + `, + '__tests__/b.test.js': `test('b', () => {});`, + 'a.js': `module.exports = {}`, + 'package.json': JSON.stringify({jest: {}}), + }); + + let stdout; + let stderr; + ({stdout, stderr} = runJest(DIR, ['--findRelatedTests', 'a.js'])); + + expect(stdout).not.toMatch('No tests found'); + expect(stderr).toMatch('PASS __tests__/a.test.js'); + expect(stderr).not.toMatch('PASS __tests__/b.test.js'); + + // change to jest.requireActual + writeFiles(DIR, { + '__tests__/a.test.js': ` + const a = jest.requireActual('../a'); + + test('a', () => {}); + `, + }); + + ({stderr, stdout} = runJest(DIR, ['--findRelatedTests', 'a.js'])); + expect(stdout).not.toMatch('No tests found'); + expect(stderr).toMatch('PASS __tests__/a.test.js'); + expect(stderr).not.toMatch('PASS __tests__/b.test.js'); +}); diff --git a/packages/jest-haste-map/src/lib/__tests__/extract_requires.test.js b/packages/jest-haste-map/src/lib/__tests__/extract_requires.test.js index b5508c676116..673829c38bbe 100644 --- a/packages/jest-haste-map/src/lib/__tests__/extract_requires.test.js +++ b/packages/jest-haste-map/src/lib/__tests__/extract_requires.test.js @@ -6,100 +6,108 @@ * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @emails oncall+jsinfra + * @flow */ 'use strict'; const extractRequires = require('../extract_requires'); -describe('extractRequires', () => { - it('extracts both requires and imports from code', () => { - const code = ` +it('extracts both requires and imports from code', () => { + const code = ` import module1 from 'module1'; const module2 = require('module2'); `; - expect(extractRequires(code)).toEqual(['module1', 'module2']); - }); + expect(extractRequires(code)).toEqual(['module1', 'module2']); +}); - it('extracts requires in order', () => { - const code = ` +it('extracts requires in order', () => { + const code = ` const module1 = require('module1'); const module2 = require('module2'); const module3 = require('module3'); `; - expect(extractRequires(code)).toEqual(['module1', 'module2', 'module3']); - }); - - it('strips out comments from code', () => { - const code = `// comment const module2 = require('module2');`; - - expect(extractRequires(code)).toEqual([]); - }); - - it('ignores requires in comments', () => { - const code = [ - '// const module1 = require("module1");', - '/**', - ' * const module2 = require("module2");', - ' */', - ].join('\n'); - - expect(extractRequires(code)).toEqual([]); - }); - - it('ignores requires in comments with Windows line endings', () => { - const code = [ - '// const module1 = require("module1");', - '/**', - ' * const module2 = require("module2");', - ' */', - ].join('\r\n'); - - expect(extractRequires(code)).toEqual([]); - }); - - it('ignores requires in comments with unicode line endings', () => { - const code = [ - '// const module1 = require("module1");\u2028', - '// const module1 = require("module2");\u2029', - '/*\u2028', - 'const module2 = require("module3");\u2029', - ' */', - ].join(''); - - expect(extractRequires(code)).toEqual([]); - }); - - it('does not contain duplicates', () => { - const code = ` + expect(extractRequires(code)).toEqual(['module1', 'module2', 'module3']); +}); + +it('strips out comments from code', () => { + const code = `// comment const module2 = require('module2');`; + + expect(extractRequires(code)).toEqual([]); +}); + +it('ignores requires in comments', () => { + const code = [ + '// const module1 = require("module1");', + '/**', + ' * const module2 = require("module2");', + ' */', + ].join('\n'); + + expect(extractRequires(code)).toEqual([]); +}); + +it('ignores requires in comments with Windows line endings', () => { + const code = [ + '// const module1 = require("module1");', + '/**', + ' * const module2 = require("module2");', + ' */', + ].join('\r\n'); + + expect(extractRequires(code)).toEqual([]); +}); + +it('ignores requires in comments with unicode line endings', () => { + const code = [ + '// const module1 = require("module1");\u2028', + '// const module1 = require("module2");\u2029', + '/*\u2028', + 'const module2 = require("module3");\u2029', + ' */', + ].join(''); + + expect(extractRequires(code)).toEqual([]); +}); + +it('does not contain duplicates', () => { + const code = ` const module1 = require('module1'); const module1Dup = require('module1'); `; - expect(extractRequires(code)).toEqual(['module1']); - }); - - it('ignores type imports', () => { - const code = [ - "import type foo from 'bar';", - 'import type {', - ' bar,', - ' baz,', - "} from 'wham'", - ].join('\r\n'); - - expect(extractRequires(code)).toEqual([]); - }); - - it('ignores type exports', () => { - const code = [ - 'export type Foo = number;', - 'export default {}', - "export * from 'module1'", - ].join('\r\n'); - - expect(extractRequires(code)).toEqual(['module1']); - }); + expect(extractRequires(code)).toEqual(['module1']); +}); + +it('ignores type imports', () => { + const code = [ + "import type foo from 'bar';", + 'import type {', + ' bar,', + ' baz,', + "} from 'wham'", + ].join('\r\n'); + + expect(extractRequires(code)).toEqual([]); +}); + +it('ignores type exports', () => { + const code = [ + 'export type Foo = number;', + 'export default {}', + "export * from 'module1'", + ].join('\r\n'); + + expect(extractRequires(code)).toEqual(['module1']); +}); + +it('understands require.requireActual', () => { + const code = `require.requireActual('pizza');`; + expect(extractRequires(code)).toEqual(['pizza']); +}); + +it('understands jest.requireActual', () => { + const code = `jest.requireActual('whiskey');`; + expect(extractRequires(code)).toEqual(['whiskey']); }); diff --git a/packages/jest-haste-map/src/lib/extract_requires.js b/packages/jest-haste-map/src/lib/extract_requires.js index b129475c376a..4f26ea910325 100644 --- a/packages/jest-haste-map/src/lib/extract_requires.js +++ b/packages/jest-haste-map/src/lib/extract_requires.js @@ -14,7 +14,7 @@ const lineCommentRe = /\/\/.*/g; const replacePatterns = { EXPORT_RE: /(\bexport\s+(?!type )(?:[^'"]+\s+from\s+)??)(['"])([^'"]+)(\2)/g, IMPORT_RE: /(\bimport\s+(?!type )(?:[^'"]+\s+from\s+)??)(['"])([^'"]+)(\2)/g, - REQUIRE_EXTENSIONS_PATTERN: /(?:^|[^.]\s*)(\b(?:require\s*?\.\s*?(?:requireActual|requireMock)|jest\s*?\.\s*?genMockFromModule)\s*?\(\s*?)([`'"])([^`'"]+)(\2\s*?\))/g, + REQUIRE_EXTENSIONS_PATTERN: /(?:^|[^.]\s*)(\b(?:require\s*?\.\s*?(?:requireActual|requireMock)|jest\s*?\.\s*?(?:requireActual|genMockFromModule))\s*?\(\s*?)([`'"])([^`'"]+)(\2\s*?\))/g, REQUIRE_RE: /(?:^|[^.]\s*)(\brequire\s*?\(\s*?)([`'"])([^`'"]+)(\2\s*?\))/g, }; diff --git a/packages/jest-runtime/src/index.js b/packages/jest-runtime/src/index.js index 09225d4dabc8..beda7a5a3b85 100644 --- a/packages/jest-runtime/src/index.js +++ b/packages/jest-runtime/src/index.js @@ -13,6 +13,7 @@ import type {Argv} from 'types/Argv'; import type {Glob, Path, ProjectConfig} from 'types/Config'; import type {Environment} from 'types/Environment'; import type {Context} from 'types/Context'; +import type {Jest, LocalModuleRequire} from 'types/Jest'; import type {ModuleMap} from 'jest-haste-map'; import type {MockFunctionMetadata, ModuleMocker} from 'types/Mock'; @@ -524,7 +525,11 @@ class Runtime { dirname, // __dirname filename, // __filename this._environment.global, // global object - this._createJestObjectFor(filename), // jest object + this._createJestObjectFor( + filename, + // $FlowFixMe + (localModule.require: LocalModuleRequire), + ), // jest object ); this._isCurrentlyExecutingManualMock = origCurrExecutingManualMock; @@ -637,7 +642,10 @@ class Runtime { return (this._shouldMockModuleCache[moduleID] = true); } - _createRequireImplementation(from: Path, options: ?InternalModuleOptions) { + _createRequireImplementation( + from: Path, + options: ?InternalModuleOptions, + ): LocalModuleRequire { const moduleRequire = options && options.isInternalModule ? (moduleName: string) => this.requireInternalModule(from, moduleName) @@ -650,14 +658,14 @@ class Runtime { return moduleRequire; } - _createJestObjectFor(from: Path) { + _createJestObjectFor(from: Path, localRequire: LocalModuleRequire): Jest { const disableAutomock = () => { this._shouldAutoMock = false; - return runtime; + return jestObject; }; const enableAutomock = () => { this._shouldAutoMock = true; - return runtime; + return jestObject; }; const unmock = (moduleName: string) => { const moduleID = this._resolver.getModuleID( @@ -666,7 +674,7 @@ class Runtime { moduleName, ); this._explicitShouldMock[moduleID] = false; - return runtime; + return jestObject; }; const deepUnmock = (moduleName: string) => { const moduleID = this._resolver.getModuleID( @@ -676,12 +684,12 @@ class Runtime { ); this._explicitShouldMock[moduleID] = false; this._transitiveShouldMock[moduleID] = false; - return runtime; + return jestObject; }; const mock = ( moduleName: string, - mockFactory: Object, - options: {virtual: boolean}, + mockFactory?: Object, + options?: {virtual: boolean}, ) => { if (mockFactory !== undefined) { return setMockFactory(moduleName, mockFactory, options); @@ -693,31 +701,31 @@ class Runtime { moduleName, ); this._explicitShouldMock[moduleID] = true; - return runtime; + return jestObject; }; const setMockFactory = (moduleName, mockFactory, options) => { this.setMock(from, moduleName, mockFactory, options); - return runtime; + return jestObject; }; const clearAllMocks = () => { this.clearAllMocks(); - return runtime; + return jestObject; }; const resetAllMocks = () => { this.resetAllMocks(); - return runtime; + return jestObject; }; const useFakeTimers = () => { this._environment.fakeTimers.useFakeTimers(); - return runtime; + return jestObject; }; const useRealTimers = () => { this._environment.fakeTimers.useRealTimers(); - return runtime; + return jestObject; }; const resetModules = () => { this.resetModules(); - return runtime; + return jestObject; }; const fn = this._moduleMocker.fn.bind(this._moduleMocker); const spyOn = this._moduleMocker.spyOn.bind(this._moduleMocker); @@ -728,9 +736,10 @@ class Runtime { : (this._environment.global[ Symbol.for('TEST_TIMEOUT_SYMBOL') ] = timeout); + return jestObject; }; - const runtime = { + const jestObject = { addMatchers: (matchers: Object) => this._environment.global.jasmine.addMatchers(matchers), @@ -751,6 +760,7 @@ class Runtime { isMockFunction: this._moduleMocker.isMockFunction, mock, + requireActual: localRequire.requireActual, resetAllMocks, resetModuleRegistry: resetModules, resetModules, @@ -773,7 +783,7 @@ class Runtime { useFakeTimers, useRealTimers, }; - return runtime; + return jestObject; } } diff --git a/types/Jest.js b/types/Jest.js new file mode 100644 index 000000000000..d33d1a22505a --- /dev/null +++ b/types/Jest.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +type GenMockFn = (implementation?: Function) => JestMockFn; +type JestMockFn = Function; + +export type LocalModuleRequire = (moduleName: string) => any; + +export type Jest = {| + addMatchers(matchers: Object): void, + autoMockOff(): Jest, + autoMockOn(): Jest, + clearAllMocks(): Jest, + clearAllTimers(): void, + deepUnmock(moduleName: string): Jest, + disableAutomock(): Jest, + doMock(moduleName: string, moduleFactory?: any): Jest, + dontMock(moduleName: string): Jest, + enableAutomock(): Jest, + fn: GenMockFn, + genMockFn: GenMockFn, + genMockFromModule(moduleName: string): any, + genMockFunction: GenMockFn, + isMockFunction(fn: Function): boolean, + mock(moduleName: string, moduleFactory?: any, options?: Object): Jest, + requireActual: LocalModuleRequire, + resetAllMocks(): Jest, + resetModuleRegistry(): Jest, + resetModules(): Jest, + runAllImmediates(): void, + runAllTicks(): void, + runAllTimers(): void, + runOnlyPendingTimers(): void, + runTimersToTime(msToRun: number): void, + setMock(moduleName: string, moduleExports: any): Jest, + setTimeout(timeout: number): Jest, + spyOn(object: Object, methodName: string): JestMockFn, + unmock(moduleName: string): Jest, + useFakeTimers(): Jest, + useRealTimers(): Jest, +|};