diff --git a/CHANGELOG.md b/CHANGELOG.md index d7c85530b170..d0a22f26256c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - `[expect]`: Improve report when matcher fails, part 10 ([#7960](https://github.com/facebook/jest/pull/7960)) - `[pretty-format]` Support `React.memo` ([#7891](https://github.com/facebook/jest/pull/7891)) - `[jest-config]` Print error information on preset normalization error ([#7935](https://github.com/facebook/jest/pull/7935)) +- `[jest-haste-map]` Add `skipPackageJson` option ([#7778](https://github.com/facebook/jest/pull/7778)) +- `[jest-get-type]` Add `isPrimitive` function ([#7708](https://github.com/facebook/jest/pull/7708)) ### Fixes @@ -22,10 +24,14 @@ - `[jest-transform]` Normalize config and remove unecessary checks, convert `TestUtils.js` to TypeScript ([#7801](https://github.com/facebook/jest/pull/7801)) - `[jest-worker]` Fix `jest-worker` when using pre-allocated jobs ([#7934](https://github.com/facebook/jest/pull/7934)) - `[jest-changed-files]` Fix `getChangedFilesFromRoots` to not return parts of the commit messages as if they were files, when the commit messages contained multiple paragraphs ([#7961](https://github.com/facebook/jest/pull/7961)) +- `[jest-haste-map]` Enforce uniqueness in names (mocks and haste ids) ([#8002](https://github.com/facebook/jest/pull/8002)) - `[static]` Remove console log '-' on the front page +- `[jest-jasmine2]`: Throw explicit error when errors happen after test is considered complete ([#8005](https://github.com/facebook/jest/pull/8005)) +- `[jest-circus]`: Throw explicit error when errors happen after test is considered complete ([#8005](https://github.com/facebook/jest/pull/8005)) ### Chore & Maintenance +- `[jest-environment-jsdom]`: Migrate to TypeScript ([#7985](https://github.com/facebook/jest/pull/8003)) - `[jest-environment-node]`: Migrate to TypeScript ([#7985](https://github.com/facebook/jest/pull/7985)) - `[*]`: Setup building, linting and testing of TypeScript ([#7808](https://github.com/facebook/jest/pull/7808), [#7855](https://github.com/facebook/jest/pull/7855), [#7951](https://github.com/facebook/jest/pull/7951)) - `[pretty-format]`: Migrate to TypeScript ([#7809](https://github.com/facebook/jest/pull/7809), [#7809](https://github.com/facebook/jest/pull/7972)) @@ -62,6 +68,7 @@ - `[jest-runner]`: Migrate to TypeScript ([#7968](https://github.com/facebook/jest/pull/7968)) - `[jest-runtime]`: Migrate to TypeScript ([#7964](https://github.com/facebook/jest/pull/7964), [#7988](https://github.com/facebook/jest/pull/7988)) - `[@jest/fake-timers]`: Extract FakeTimers class from `jest-util` into a new separate package ([#7987](https://github.com/facebook/jest/pull/7987)) +- `[jest-repl]`: Migrate to TypeScript ([#8000](https://github.com/facebook/jest/pull/8000)) - `[@jest/core]`: Migrate to TypeScript ([#7998](https://github.com/facebook/jest/pull/7998)) ### Performance @@ -93,6 +100,7 @@ - `[jest-runtime]` Exclude setup/teardown files from coverage report ([#7790](https://github.com/facebook/jest/pull/7790) - `[babel-jest]` Throw an error if `babel-jest` tries to transform a file ignored by Babel ([#7797](https://github.com/facebook/jest/pull/7797)) - `[babel-plugin-jest-hoist]` Ignore TS type references when looking for out-of-scope references ([#7799](https://github.com/facebook/jest/pull/7799) +- `[expect]` fixed asymmetrical equality of cyclic objects ([#7730](https://github.com/facebook/jest/pull/7730)) ### Chore & Maintenance diff --git a/e2e/__tests__/detectOpenHandles.ts b/e2e/__tests__/detectOpenHandles.ts index f6b4503e980f..70420b49e21d 100644 --- a/e2e/__tests__/detectOpenHandles.ts +++ b/e2e/__tests__/detectOpenHandles.ts @@ -22,7 +22,7 @@ try { } } -function getTextAfterTest(stderr) { +function getTextAfterTest(stderr: string) { return (stderr.split(/Ran all test suites(.*)\n/)[2] || '').trim(); } diff --git a/e2e/__tests__/failures.test.ts b/e2e/__tests__/failures.test.ts index f99bed6a4938..851bb3d1a39b 100644 --- a/e2e/__tests__/failures.test.ts +++ b/e2e/__tests__/failures.test.ts @@ -12,9 +12,9 @@ import runJest from '../runJest'; const dir = path.resolve(__dirname, '../failures'); -const normalizeDots = text => text.replace(/\.{1,}$/gm, '.'); +const normalizeDots = (text: string) => text.replace(/\.{1,}$/gm, '.'); -function cleanStderr(stderr) { +function cleanStderr(stderr: string) { const {rest} = extractSummary(stderr); return rest .replace(/.*(jest-jasmine2|jest-circus).*\n/g, '') @@ -182,3 +182,12 @@ test('works with named snapshot failures', () => { wrap(result.substring(0, result.indexOf('Snapshot Summary'))), ).toMatchSnapshot(); }); + +test('errors after test has completed', () => { + const {stderr} = runJest(dir, ['errorAfterTestComplete.test.js']); + + expect(stderr).toMatch( + /Error: Caught error after test environment was torn down/, + ); + expect(stderr).toMatch(/Failed: "fail async"/); +}); diff --git a/e2e/failures/__tests__/errorAfterTestComplete.test.js b/e2e/failures/__tests__/errorAfterTestComplete.test.js new file mode 100644 index 000000000000..6bfb7dc3ae79 --- /dev/null +++ b/e2e/failures/__tests__/errorAfterTestComplete.test.js @@ -0,0 +1,14 @@ +/** + * 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. + * + * @emails oncall+jsinfra + */ +'use strict'; + +test('a failing test', done => { + setTimeout(() => done('fail async'), 5); + done(); +}); diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index 4e2cf535b6b3..cff90804df79 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -606,6 +606,59 @@ describe('.toEqual()', () => { }); expect(actual).toEqual({x: 3}); }); + + describe('cyclic object equality', () => { + test('properties with the same circularity are equal', () => { + const a = {}; + a.x = a; + const b = {}; + b.x = b; + expect(a).toEqual(b); + expect(b).toEqual(a); + + const c = {}; + c.x = a; + const d = {}; + d.x = b; + expect(c).toEqual(d); + expect(d).toEqual(c); + }); + + test('properties with different circularity are not equal', () => { + const a = {}; + a.x = {y: a}; + const b = {}; + const bx = {}; + b.x = bx; + bx.y = bx; + expect(a).not.toEqual(b); + expect(b).not.toEqual(a); + + const c = {}; + c.x = a; + const d = {}; + d.x = b; + expect(c).not.toEqual(d); + expect(d).not.toEqual(c); + }); + + test('are not equal if circularity is not on the same property', () => { + const a = {}; + const b = {}; + a.a = a; + b.a = {}; + b.a.a = a; + expect(a).not.toEqual(b); + expect(b).not.toEqual(a); + + const c = {}; + c.x = {x: c}; + const d = {}; + d.x = d; + expect(c).not.toEqual(d); + expect(d).not.toEqual(c); + }); + }); }); describe('.toBeInstanceOf()', () => { diff --git a/packages/expect/src/jasmineUtils.ts b/packages/expect/src/jasmineUtils.ts index 11e368d11001..f16bc5aeaa64 100644 --- a/packages/expect/src/jasmineUtils.ts +++ b/packages/expect/src/jasmineUtils.ts @@ -64,9 +64,9 @@ function asymmetricMatch(a: any, b: any) { function eq( a: any, b: any, - aStack: any, - bStack: any, - customTesters: any, + aStack: Array, + bStack: Array, + customTesters: Array, hasKey: any, ): boolean { var result = true; @@ -149,14 +149,17 @@ function eq( return false; } - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + // Used to detect circular references. var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. - if (aStack[length] == a) { - return bStack[length] == b; + // circular references at same depth are equal + // circular reference is not equal to non-circular one + if (aStack[length] === a) { + return bStack[length] === b; + } else if (bStack[length] === b) { + return false; } } // Add the first object to the stack of traversed objects. diff --git a/packages/jest-circus/src/utils.ts b/packages/jest-circus/src/utils.ts index 7917d1356e9d..fbad7a725c77 100644 --- a/packages/jest-circus/src/utils.ts +++ b/packages/jest-circus/src/utils.ts @@ -153,12 +153,17 @@ const _makeTimeoutMessage = (timeout: number, isHook: boolean) => // the original values in the variables before we require any files. const {setTimeout, clearTimeout} = global; +function checkIsError(error: any): error is Error { + return !!(error && (error as Error).message && (error as Error).stack); +} + export const callAsyncCircusFn = ( fn: AsyncFn, testContext: TestContext | undefined, {isHook, timeout}: {isHook?: boolean | null; timeout: number}, ): Promise => { let timeoutID: NodeJS.Timeout; + let completed = false; return new Promise((resolve, reject) => { timeoutID = setTimeout( @@ -170,15 +175,20 @@ export const callAsyncCircusFn = ( // soon as `done` called. if (fn.length) { const done = (reason?: Error | string): void => { - const isError = - reason && (reason as Error).message && (reason as Error).stack; - return reason - ? reject( - isError - ? reason - : new Error(`Failed: ${prettyFormat(reason, {maxDepth: 3})}`), - ) - : resolve(); + const errorAsErrorObject = checkIsError(reason) + ? reason + : new Error(`Failed: ${prettyFormat(reason, {maxDepth: 3})}`); + + // Consider always throwing, regardless if `reason` is set or not + if (completed && reason) { + errorAsErrorObject.message = + 'Caught error after test environment was torn down\n\n' + + errorAsErrorObject.message; + + throw errorAsErrorObject; + } + + return reason ? reject(errorAsErrorObject) : resolve(); }; return fn.call(testContext, done); @@ -221,12 +231,14 @@ export const callAsyncCircusFn = ( return resolve(); }) .then(() => { + completed = true; // If timeout is not cleared/unrefed the node process won't exit until // it's resolved. timeoutID.unref && timeoutID.unref(); clearTimeout(timeoutID); }) .catch(error => { + completed = true; timeoutID.unref && timeoutID.unref(); clearTimeout(timeoutID); throw error; diff --git a/packages/jest-config/tsconfig.json b/packages/jest-config/tsconfig.json index 55f2ed3ff353..16502ade2786 100644 --- a/packages/jest-config/tsconfig.json +++ b/packages/jest-config/tsconfig.json @@ -4,10 +4,10 @@ "rootDir": "src", "outDir": "build" }, - // TODO: This is missing `jest-validate`, in addition to - // `jest-environment-jsdom` and `jest-jasmine2`, but those are just - // `require.resolve`d, so no real use for their types + // TODO: This is missing `jest-validate`, in addition to and `jest-jasmine2`, + // but those are just `require.resolve`d, so no real use for their types "references": [ + {"path": "../jest-environment-jsdom"}, {"path": "../jest-environment-node"}, {"path": "../jest-get-type"}, {"path": "../jest-regex-util"}, diff --git a/packages/jest-each/src/bind.js b/packages/jest-each/src/bind.js index b09a0dff20f3..8b8d95aa4f54 100644 --- a/packages/jest-each/src/bind.js +++ b/packages/jest-each/src/bind.js @@ -10,7 +10,7 @@ import util from 'util'; import chalk from 'chalk'; import pretty from 'pretty-format'; -import getType from 'jest-get-type'; +import {isPrimitive} from 'jest-get-type'; import {ErrorWithStack} from 'jest-util'; type Table = Array>; @@ -24,13 +24,6 @@ const RECEIVED_COLOR = chalk.red; const SUPPORTED_PLACEHOLDERS = /%[sdifjoOp%]/g; const PRETTY_PLACEHOLDER = '%p'; const INDEX_PLACEHOLDER = '%#'; -const PRIMITIVES = new Set([ - 'string', - 'number', - 'boolean', - 'null', - 'undefined', -]); export default (cb: Function, supportsDone: boolean = true) => (...args: any) => function eachBind(title: string, test: Function, timeout: number): void { @@ -203,10 +196,9 @@ const getMatchingKeyPaths = title => (matches, key) => const replaceKeyPathWithValue = data => (title, match) => { const keyPath = match.replace('$', '').split('.'); const value = getPath(data, keyPath); - const valueType = getType(value); - if (PRIMITIVES.has(valueType)) { - return title.replace(match, value); + if (isPrimitive(value)) { + return title.replace(match, String(value)); } return title.replace(match, pretty(value, {maxDepth: 1, min: true})); }; diff --git a/packages/jest-environment-jsdom/package.json b/packages/jest-environment-jsdom/package.json index 65799a5f7909..25c3d65c6a04 100644 --- a/packages/jest-environment-jsdom/package.json +++ b/packages/jest-environment-jsdom/package.json @@ -8,13 +8,16 @@ }, "license": "MIT", "main": "build/index.js", + "types": "build/index.d.ts", "dependencies": { + "@jest/environment": "^24.1.0", "@jest/fake-timers": "^24.1.0", + "@jest/types": "^24.1.0", "jest-mock": "^24.0.0", "jest-util": "^24.0.0", "jsdom": "^11.5.1" }, - "devDependencies": { + "devDependencies": { "@types/jsdom": "^11.12.0" }, "engines": { diff --git a/packages/jest-environment-jsdom/src/__mocks__/index.js b/packages/jest-environment-jsdom/src/__mocks__/index.ts similarity index 81% rename from packages/jest-environment-jsdom/src/__mocks__/index.js rename to packages/jest-environment-jsdom/src/__mocks__/index.ts index f2bf9a30476e..b528bc840c64 100644 --- a/packages/jest-environment-jsdom/src/__mocks__/index.js +++ b/packages/jest-environment-jsdom/src/__mocks__/index.ts @@ -8,9 +8,10 @@ const vm = jest.requireActual('vm'); -const JSDOMEnvironment = jest.genMockFromModule('../index'); +const JSDOMEnvironment = jest.genMockFromModule('../index') as jest.Mock; JSDOMEnvironment.mockImplementation(function(config) { + // @ts-ignore this.global = { JSON, console: {}, @@ -18,14 +19,16 @@ JSDOMEnvironment.mockImplementation(function(config) { const globalValues = {...config.globals}; for (const customGlobalKey in globalValues) { + // @ts-ignore this.global[customGlobalKey] = globalValues[customGlobalKey]; } }); JSDOMEnvironment.prototype.runSourceText.mockImplementation(function( - sourceText, - filename, + sourceText: string, + filename: string, ) { + // @ts-ignore return vm.runInNewContext(sourceText, this.global, { displayErrors: false, filename, diff --git a/packages/jest-environment-jsdom/src/__tests__/jsdom_environment.test.js b/packages/jest-environment-jsdom/src/__tests__/jsdom_environment.test.ts similarity index 100% rename from packages/jest-environment-jsdom/src/__tests__/jsdom_environment.test.js rename to packages/jest-environment-jsdom/src/__tests__/jsdom_environment.test.ts diff --git a/packages/jest-environment-jsdom/src/index.js b/packages/jest-environment-jsdom/src/index.ts similarity index 55% rename from packages/jest-environment-jsdom/src/index.js rename to packages/jest-environment-jsdom/src/index.ts index 56f417ba8188..8d0c33c1df7c 100644 --- a/packages/jest-environment-jsdom/src/index.js +++ b/packages/jest-environment-jsdom/src/index.ts @@ -3,28 +3,40 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * @flow */ -import type {Script} from 'vm'; -import type {ProjectConfig} from 'types/Config'; -import type {EnvironmentContext} from 'types/Environment'; -import type {Global} from 'types/Global'; -import type {ModuleMocker} from 'jest-mock'; - -import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; +import {Script} from 'vm'; +import {Global, Config} from '@jest/types'; import {installCommonGlobals} from 'jest-util'; -import mock from 'jest-mock'; +import mock, {ModuleMocker} from 'jest-mock'; +import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; +import {JestEnvironment, EnvironmentContext} from '@jest/environment'; import {JSDOM, VirtualConsole} from 'jsdom'; -class JSDOMEnvironment { - dom: ?Object; - fakeTimers: ?FakeTimers; - global: ?Global; - errorEventListener: ?Function; - moduleMocker: ?ModuleMocker; +// The `Window` interface does not have an `Error.stackTraceLimit` property, but +// `JSDOMEnvironment` assumes it is there. +interface Win extends Window { + Error: { + stackTraceLimit: number; + }; +} + +function isWin(globals: Win | Global.Global): globals is Win { + return (globals as Win).document !== undefined; +} + +// A lot of the globals expected by other APIs are `NodeJS.Global` and not +// `Window`, so we need to cast here and there + +class JSDOMEnvironment implements JestEnvironment { + dom: JSDOM | null; + fakeTimers: FakeTimers | null; + // @ts-ignore + global: Global.Global | Win | null; + errorEventListener: ((event: Event & {error: Error}) => void) | null; + moduleMocker: ModuleMocker | null; - constructor(config: ProjectConfig, options?: EnvironmentContext = {}) { + constructor(config: Config.ProjectConfig, options: EnvironmentContext = {}) { this.dom = new JSDOM('', { pretendToBeVisual: true, runScripts: 'dangerously', @@ -32,11 +44,16 @@ class JSDOMEnvironment { virtualConsole: new VirtualConsole().sendTo(options.console || console), ...config.testEnvironmentOptions, }); - const global = (this.global = this.dom.window.document.defaultView); + const global = (this.global = this.dom.window.document.defaultView as Win); + + if (!global) { + throw new Error('JSDOM did not return a Window object'); + } + // Node's error-message stack size is limited at 10, but it's pretty useful // to see more than that when a test fails. this.global.Error.stackTraceLimit = 100; - installCommonGlobals(global, config.globals); + installCommonGlobals(global as any, config.globals); // Report uncaught errors. this.errorEventListener = event => { @@ -51,20 +68,24 @@ class JSDOMEnvironment { const originalAddListener = global.addEventListener; const originalRemoveListener = global.removeEventListener; let userErrorListenerCount = 0; - global.addEventListener = function(name) { - if (name === 'error') { + global.addEventListener = function( + ...args: Parameters + ) { + if (args[0] === 'error') { userErrorListenerCount++; } - return originalAddListener.apply(this, arguments); + return originalAddListener.apply(this, args); }; - global.removeEventListener = function(name) { - if (name === 'error') { + global.removeEventListener = function( + ...args: Parameters + ) { + if (args[0] === 'error') { userErrorListenerCount--; } - return originalRemoveListener.apply(this, arguments); + return originalRemoveListener.apply(this, args); }; - this.moduleMocker = new mock.ModuleMocker(global); + this.moduleMocker = new mock.ModuleMocker(global as any); const timerConfig = { idToRef: (id: number) => id, @@ -73,27 +94,29 @@ class JSDOMEnvironment { this.fakeTimers = new FakeTimers({ config, - global, + global: global as any, moduleMocker: this.moduleMocker, timerConfig, }); } - setup(): Promise { + setup() { return Promise.resolve(); } - teardown(): Promise { + teardown() { if (this.fakeTimers) { this.fakeTimers.dispose(); } if (this.global) { - if (this.errorEventListener) { + if (this.errorEventListener && isWin(this.global)) { this.global.removeEventListener('error', this.errorEventListener); } // Dispose "document" to prevent "load" event from triggering. Object.defineProperty(this.global, 'document', {value: null}); - this.global.close(); + if (isWin(this.global)) { + this.global.close(); + } } this.errorEventListener = null; this.global = null; @@ -102,12 +125,12 @@ class JSDOMEnvironment { return Promise.resolve(); } - runScript(script: Script): ?any { + runScript(script: Script) { if (this.dom) { - return this.dom.runVMScript(script); + return this.dom.runVMScript(script) as any; } return null; } } -module.exports = JSDOMEnvironment; +export = JSDOMEnvironment; diff --git a/packages/jest-environment-jsdom/tsconfig.json b/packages/jest-environment-jsdom/tsconfig.json new file mode 100644 index 000000000000..655d8c8aab7c --- /dev/null +++ b/packages/jest-environment-jsdom/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "build", + "rootDir": "src" + }, + "references": [ + {"path": "../jest-environment"}, + {"path": "../jest-fake-timers"}, + {"path": "../jest-mock"}, + {"path": "../jest-types"}, + {"path": "../jest-util"} + ] +} diff --git a/packages/jest-get-type/src/__tests__/index.test.ts b/packages/jest-get-type/src/__tests__/getType.test.ts similarity index 97% rename from packages/jest-get-type/src/__tests__/index.test.ts rename to packages/jest-get-type/src/__tests__/getType.test.ts index 8a34a0cb947d..de4bec59d8db 100644 --- a/packages/jest-get-type/src/__tests__/index.test.ts +++ b/packages/jest-get-type/src/__tests__/getType.test.ts @@ -6,7 +6,7 @@ * */ -import getType from '..'; +import getType from '../'; describe('.getType()', () => { test('null', () => expect(getType(null)).toBe('null')); diff --git a/packages/jest-get-type/src/__tests__/isPrimitive.test.ts b/packages/jest-get-type/src/__tests__/isPrimitive.test.ts new file mode 100644 index 000000000000..d3e9a7175af5 --- /dev/null +++ b/packages/jest-get-type/src/__tests__/isPrimitive.test.ts @@ -0,0 +1,25 @@ +/** + * 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 {isPrimitive} from '..'; + +describe('.isPrimitive()', () => { + test.each([null, undefined, 100, 'hello world', true, Symbol.for('a')])( + 'returns true when given primitive value of: %s', + primitive => { + expect(isPrimitive(primitive)).toBe(true); + }, + ); + + test.each([{}, [], () => {}, /abc/, new Map(), new Set(), new Date()])( + 'returns false when given non primitive value of: %s', + value => { + expect(isPrimitive(value)).toBe(false); + }, + ); +}); diff --git a/packages/jest-get-type/src/index.ts b/packages/jest-get-type/src/index.ts index dd15b71be771..cfea04eaf48a 100644 --- a/packages/jest-get-type/src/index.ts +++ b/packages/jest-get-type/src/index.ts @@ -20,9 +20,18 @@ type ValueType = | 'symbol' | 'undefined'; +const PRIMITIVES = new Set([ + 'string', + 'number', + 'boolean', + 'null', + 'undefined', + 'symbol', +]); + // get the type of a value with handling the edge cases like `typeof []` // and `typeof null` -const getType = (value: unknown): ValueType => { +function getType(value: unknown): ValueType { if (value === undefined) { return 'undefined'; } else if (value === null) { @@ -55,6 +64,8 @@ const getType = (value: unknown): ValueType => { } throw new Error(`value of unknown type: ${value}`); -}; +} + +getType.isPrimitive = (value: unknown) => PRIMITIVES.has(getType(value)); export = getType; diff --git a/packages/jest-haste-map/src/__tests__/__snapshots__/index.test.js.snap b/packages/jest-haste-map/src/__tests__/__snapshots__/index.test.js.snap index d86bc28499f3..82841ac1ed01 100644 --- a/packages/jest-haste-map/src/__tests__/__snapshots__/index.test.js.snap +++ b/packages/jest-haste-map/src/__tests__/__snapshots__/index.test.js.snap @@ -16,13 +16,7 @@ exports[`HasteMap file system changes processing recovery from duplicate module " `; -exports[`HasteMap throws on duplicate module ids if "throwOnModuleCollision" is set to true 1`] = ` -[Error: jest-haste-map: Haste module naming collision: - Duplicate module name: Strawberry - Paths: /project/fruits/another/Strawberry.js collides with /project/fruits/Strawberry.js - -This error is caused by \`hasteImpl\` returning the same name for different files.] -`; +exports[`HasteMap throws on duplicate module ids if "throwOnModuleCollision" is set to true 1`] = `[Error: Duplicated files or mocks. Please check the console for more info]`; exports[`HasteMap tries to crawl using node as a fallback 1`] = ` "jest-haste-map: Watchman crawl failed. Retrying once with node crawler. @@ -31,23 +25,17 @@ exports[`HasteMap tries to crawl using node as a fallback 1`] = ` `; exports[`HasteMap warns on duplicate mock files 1`] = ` -"jest-haste-map: duplicate manual mock found: - Module name: subdir/Blueberry - Duplicate Mock path: /project/fruits2/__mocks__/subdir/Blueberry.js -This warning is caused by two manual mock files with the same file name. -Jest will use the mock file found in: -/project/fruits2/__mocks__/subdir/Blueberry.js - Please delete one of the following two files: - /project/fruits1/__mocks__/subdir/Blueberry.js -/project/fruits2/__mocks__/subdir/Blueberry.js - +"jest-haste-map: duplicate manual mock found: subdir/Blueberry + The following files share their name; please delete one of them: + * /fruits1/__mocks__/subdir/Blueberry.js + * /fruits2/__mocks__/subdir/Blueberry.js " `; exports[`HasteMap warns on duplicate module ids 1`] = ` -"jest-haste-map: Haste module naming collision: - Duplicate module name: Strawberry - Paths: /project/fruits/other/Strawberry.js collides with /project/fruits/Strawberry.js - -This warning is caused by \`hasteImpl\` returning the same name for different files." +"jest-haste-map: Haste module naming collision: Strawberry + The following files share their name; please adjust your hasteImpl: + * /fruits/Strawberry.js + * /fruits/other/Strawberry.js +" `; diff --git a/packages/jest-haste-map/src/__tests__/index.test.js b/packages/jest-haste-map/src/__tests__/index.test.js index e200fb612b6b..68b3aa4e226b 100644 --- a/packages/jest-haste-map/src/__tests__/index.test.js +++ b/packages/jest-haste-map/src/__tests__/index.test.js @@ -128,6 +128,7 @@ const useBuitinsInContext = value => { }; let consoleWarn; +let consoleError; let defaultConfig; let fs; let H; @@ -180,7 +181,10 @@ describe('HasteMap', () => { fs = require('graceful-fs'); consoleWarn = console.warn; + consoleError = console.error; + console.warn = jest.fn(); + console.error = jest.fn(); HasteMap = require('../'); H = HasteMap.H; @@ -203,6 +207,7 @@ describe('HasteMap', () => { afterEach(() => { console.warn = consoleWarn; + console.error = consoleError; }); it('exports constants', () => { @@ -522,6 +527,8 @@ describe('HasteMap', () => { }); it('warns on duplicate mock files', () => { + expect.assertions(1); + // Duplicate mock files for blueberry mockFs['/project/fruits1/__mocks__/subdir/Blueberry.js'] = ` // Blueberry @@ -530,10 +537,14 @@ describe('HasteMap', () => { // Blueberry too! `; - return new HasteMap({mocksPattern: '__mocks__', ...defaultConfig}) + return new HasteMap({ + mocksPattern: '__mocks__', + throwOnModuleCollision: true, + ...defaultConfig, + }) .build() - .then(({__hasteMapForTest: data}) => { - expect(console.warn.mock.calls[0][0]).toMatchSnapshot(); + .catch(({__hasteMapForTest: data}) => { + expect(console.error.mock.calls[0][0]).toMatchSnapshot(); }); }); diff --git a/packages/jest-haste-map/src/index.ts b/packages/jest-haste-map/src/index.ts index 3a7d1926539e..88a0aa893d00 100644 --- a/packages/jest-haste-map/src/index.ts +++ b/packages/jest-haste-map/src/index.ts @@ -66,6 +66,7 @@ type Options = { retainAllFiles: boolean; rootDir: string; roots: Array; + skipPackageJson?: boolean; throwOnModuleCollision?: boolean; useWatchman?: boolean; watch?: boolean; @@ -89,6 +90,7 @@ type InternalOptions = { retainAllFiles: boolean; rootDir: string; roots: Array; + skipPackageJson: boolean; throwOnModuleCollision: boolean; useWatchman: boolean; watch: boolean; @@ -112,6 +114,7 @@ namespace HasteMap { const CHANGE_INTERVAL = 30; const MAX_WAIT_TIME = 240000; const NODE_MODULES = path.sep + 'node_modules' + path.sep; +const PACKAGE_JSON = path.sep + 'package.json'; // TypeScript doesn't like us importing from outside `rootDir`, but it doesn't // understand `require`. @@ -260,6 +263,7 @@ class HasteMap extends EventEmitter { retainAllFiles: options.retainAllFiles, rootDir: options.rootDir, roots: Array.from(new Set(options.roots)), + skipPackageJson: !!options.skipPackageJson, throwOnModuleCollision: !!options.throwOnModuleCollision, useWatchman: options.useWatchman == null ? true : options.useWatchman, watch: !!options.watch, @@ -436,27 +440,31 @@ class HasteMap extends EventEmitter { H.GENERIC_PLATFORM; const existingModule = moduleMap[platform]; + if (existingModule && existingModule[H.PATH] !== module[H.PATH]) { - const message = - `jest-haste-map: Haste module naming collision:\n` + - ` Duplicate module name: ${id}\n` + - ` Paths: ${fastPath.resolve( - rootDir, - module[H.PATH], - )} collides with ` + - `${fastPath.resolve(rootDir, existingModule[H.PATH])}\n\nThis ` + - `${this._options.throwOnModuleCollision ? 'error' : 'warning'} ` + - `is caused by \`hasteImpl\` returning the same name for different` + - ` files.`; + const method = this._options.throwOnModuleCollision ? 'error' : 'warn'; + + this._console[method]( + [ + 'jest-haste-map: Haste module naming collision: ' + id, + ' The following files share their name; please adjust your hasteImpl:', + ' * ' + path.sep + existingModule[H.PATH], + ' * ' + path.sep + module[H.PATH], + '', + ].join('\n'), + ); + if (this._options.throwOnModuleCollision) { - throw new Error(message); + throw new DuplicateError(existingModule[H.PATH], module[H.PATH]); } - this._console.warn(message); + // We do NOT want consumers to use a module that is ambiguous. delete moduleMap[platform]; + if (Object.keys(moduleMap).length === 1) { map.delete(id); } + let dupsByPlatform = hasteMap.duplicates.get(id); if (dupsByPlatform == null) { dupsByPlatform = new Map(); @@ -557,18 +565,26 @@ class HasteMap extends EventEmitter { ) { const mockPath = getMockName(filePath); const existingMockPath = mocks.get(mockPath); + if (existingMockPath) { - this._console.warn( - `jest-haste-map: duplicate manual mock found:\n` + - ` Module name: ${mockPath}\n` + - ` Duplicate Mock path: ${filePath}\nThis warning ` + - `is caused by two manual mock files with the same file name.\n` + - `Jest will use the mock file found in: \n` + - `${filePath}\n` + - ` Please delete one of the following two files: \n ` + - `${path.join(rootDir, existingMockPath)}\n${filePath}\n\n`, + const secondMockPath = fastPath.relative(rootDir, filePath); + const method = this._options.throwOnModuleCollision ? 'error' : 'warn'; + + this._console[method]( + [ + 'jest-haste-map: duplicate manual mock found: ' + mockPath, + ' The following files share their name; please delete one of them:', + ' * ' + path.sep + existingMockPath, + ' * ' + path.sep + secondMockPath, + '', + ].join('\n'), ); + + if (this._options.throwOnModuleCollision) { + throw new DuplicateError(existingMockPath, secondMockPath); + } } + mocks.set(mockPath, relativeFilePath); } @@ -627,6 +643,12 @@ class HasteMap extends EventEmitter { } for (const relativeFilePath of hasteMap.files.keys()) { + if ( + this._options.skipPackageJson && + relativeFilePath.endsWith(PACKAGE_JSON) + ) { + continue; + } // SHA-1, if requested, should already be present thanks to the crawler. const filePath = fastPath.resolve( this._options.rootDir, @@ -1070,9 +1092,22 @@ class HasteMap extends EventEmitter { } static H: HType; + static DuplicateError: typeof DuplicateError; static ModuleMap: typeof HasteModuleMap; } +class DuplicateError extends Error { + mockPath1: string; + mockPath2: string; + + constructor(mockPath1: string, mockPath2: string) { + super('Duplicated files or mocks. Please check the console for more info'); + + this.mockPath1 = mockPath1; + this.mockPath2 = mockPath2; + } +} + function copy(object: T): T { return Object.assign(Object.create(null), object); } @@ -1082,6 +1117,7 @@ function copyMap(input: Map): Map { } HasteMap.H = H; +HasteMap.DuplicateError = DuplicateError; HasteMap.ModuleMap = HasteModuleMap; export = HasteMap; diff --git a/packages/jest-jasmine2/src/jasmine/Env.js b/packages/jest-jasmine2/src/jasmine/Env.js index 49e8963980b7..c1e6b70992b0 100644 --- a/packages/jest-jasmine2/src/jasmine/Env.js +++ b/packages/jest-jasmine2/src/jasmine/Env.js @@ -586,13 +586,24 @@ export default function(j$) { message = check.message; } - currentRunnable().addExpectationResult(false, { + const errorAsErrorObject = checkIsError ? error : new Error(message); + const runnable = currentRunnable(); + + if (!runnable) { + errorAsErrorObject.message = + 'Caught error after test environment was torn down\n\n' + + errorAsErrorObject.message; + + throw errorAsErrorObject; + } + + runnable.addExpectationResult(false, { matcherName: '', passed: false, expected: '', actual: '', message, - error: checkIsError ? error : new Error(message), + error: errorAsErrorObject, }); }; } diff --git a/packages/jest-repl/package.json b/packages/jest-repl/package.json index 8c9abb3ec472..f4bb47655945 100644 --- a/packages/jest-repl/package.json +++ b/packages/jest-repl/package.json @@ -8,7 +8,10 @@ }, "license": "MIT", "main": "build/index.js", + "types": "build/index.d.ts", "dependencies": { + "@jest/transform": "^24.1.0", + "@jest/types": "^24.1.0", "jest-config": "^24.1.0", "jest-runtime": "^24.1.0", "jest-validate": "^24.0.0", diff --git a/packages/jest-repl/src/cli/args.js b/packages/jest-repl/src/cli/args.ts similarity index 88% rename from packages/jest-repl/src/cli/args.js rename to packages/jest-repl/src/cli/args.ts index c5d29b013765..b0c72fd6e3c4 100644 --- a/packages/jest-repl/src/cli/args.js +++ b/packages/jest-repl/src/cli/args.ts @@ -4,15 +4,13 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ import Runtime from 'jest-runtime'; export const usage = 'Usage: $0 [--config=]'; -export const options = { - ...Runtime.getCLIOptions(), +export const options = Object.assign({}, Runtime.getCLIOptions(), { replname: { alias: 'r', description: @@ -20,4 +18,4 @@ export const options = { 'transformed. For example, "repl.ts" if using a TypeScript transformer.', type: 'string', }, -}; +}); diff --git a/packages/jest-repl/src/cli/index.js b/packages/jest-repl/src/cli/index.ts similarity index 74% rename from packages/jest-repl/src/cli/index.js rename to packages/jest-repl/src/cli/index.ts index b9a0abaf9c3b..5bbf3f975c20 100644 --- a/packages/jest-repl/src/cli/index.js +++ b/packages/jest-repl/src/cli/index.ts @@ -5,26 +5,26 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ -import path from 'path'; - import Runtime from 'jest-runtime'; import yargs from 'yargs'; +// @ts-ignore: Wait for jest-validate to get migrated import {validateCLIOptions} from 'jest-validate'; import {deprecationEntries} from 'jest-config'; -import {version as VERSION} from '../../package.json'; import * as args from './args'; -const REPL_SCRIPT = path.resolve(__dirname, './repl.js'); +const {version: VERSION} = require('../../package.json'); + +const REPL_SCRIPT = require.resolve('./repl.js'); -module.exports = function() { +export = function() { const argv = yargs.usage(args.usage).options(args.options).argv; validateCLIOptions(argv, {...args.options, deprecationEntries}); argv._ = [REPL_SCRIPT]; + // @ts-ignore: not the same arguments Runtime.runCLI(argv, [`Jest REPL v${VERSION}`]); }; diff --git a/packages/jest-repl/src/cli/repl.js b/packages/jest-repl/src/cli/repl.ts similarity index 70% rename from packages/jest-repl/src/cli/repl.js rename to packages/jest-repl/src/cli/repl.ts index 6a0b8fa5ccea..66f5725ecbed 100644 --- a/packages/jest-repl/src/cli/repl.js +++ b/packages/jest-repl/src/cli/repl.ts @@ -5,30 +5,37 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ -import type {GlobalConfig, ProjectConfig} from 'types/Config'; - -declare var jestGlobalConfig: GlobalConfig; -declare var jestProjectConfig: ProjectConfig; -declare var jest: Object; +declare const jestGlobalConfig: Config.GlobalConfig; +declare const jestProjectConfig: Config.ProjectConfig; import path from 'path'; import repl from 'repl'; import vm from 'vm'; +import {Transformer} from '@jest/transform'; +import {Config} from '@jest/types'; -let transformer; +let transformer: Transformer; -const evalCommand = (cmd, context, filename, callback, config) => { +const evalCommand: repl.REPLEval = ( + cmd: string, + _context: any, + _filename: string, + callback: (e: Error | null, result?: any) => void, +) => { let result; try { if (transformer) { - cmd = transformer.process( + const transformResult = transformer.process( cmd, jestGlobalConfig.replname || 'jest.js', jestProjectConfig, ); + cmd = + typeof transformResult === 'string' + ? transformResult + : transformResult.code; } result = vm.runInThisContext(cmd); } catch (e) { @@ -37,7 +44,7 @@ const evalCommand = (cmd, context, filename, callback, config) => { return callback(null, result); }; -const isRecoverableError = error => { +const isRecoverableError = (error: Error) => { if (error && error.name === 'SyntaxError') { return [ 'Unterminated template', @@ -59,7 +66,6 @@ if (jestProjectConfig.transform) { } } if (transformerPath) { - /* $FlowFixMe */ transformer = require(transformerPath); if (typeof transformer.process !== 'function') { throw new TypeError( @@ -69,17 +75,15 @@ if (jestProjectConfig.transform) { } } -const replInstance = repl.start({ +const replInstance: repl.REPLServer = repl.start({ eval: evalCommand, prompt: '\u203A ', useGlobal: true, }); -// $FlowFixMe: https://github.com/facebook/flow/pull/4713 -replInstance.context.require = moduleName => { +replInstance.context.require = (moduleName: string) => { if (/(\/|\\|\.)/.test(moduleName)) { moduleName = path.resolve(process.cwd(), moduleName); } - /* $FlowFixMe */ return require(moduleName); }; diff --git a/packages/jest-repl/tsconfig.json b/packages/jest-repl/tsconfig.json new file mode 100644 index 000000000000..98a536c2c508 --- /dev/null +++ b/packages/jest-repl/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build" + }, + "references": [ + {"path": "../jest-config"}, + {"path": "../jest-runtime"}, + {"path": "../jest-transform"}, + {"path": "../jest-types"} + // TODO: Wait for this to get migrated + // {"path": "../jest-validate"} + ] +} diff --git a/packages/jest-runtime/src/__tests__/runtime_require_mock.test.js b/packages/jest-runtime/src/__tests__/runtime_require_mock.test.js index b80ff9e500c5..c63697b44a25 100644 --- a/packages/jest-runtime/src/__tests__/runtime_require_mock.test.js +++ b/packages/jest-runtime/src/__tests__/runtime_require_mock.test.js @@ -8,8 +8,6 @@ 'use strict'; -const path = require('path'); - let createRuntime; const consoleWarn = console.warn; @@ -137,29 +135,6 @@ describe('Runtime', () => { }).toThrow(); })); - it('uses the closest manual mock when duplicates exist', () => { - console.warn = jest.fn(); - return createRuntime(__filename, { - rootDir: path.resolve( - path.dirname(__filename), - 'test_root_with_dup_mocks', - ), - }).then(runtime => { - expect(console.warn).toBeCalled(); - const exports1 = runtime.requireMock( - runtime.__mockRootPath, - './subdir1/my_module', - ); - expect(exports1.modulePath).toEqual('subdir1/__mocks__/my_module.js'); - - const exports2 = runtime.requireMock( - runtime.__mockRootPath, - './subdir2/my_module', - ); - expect(exports2.modulePath).toEqual('subdir2/__mocks__/my_module.js'); - }); - }); - it('uses manual mocks when using a custom resolver', () => createRuntime(__filename, { // using the default resolver as a custom resolver