From 4023b8c8e1432ed788d33df48b69794c422e4880 Mon Sep 17 00:00:00 2001 From: Giorgi Rostomashvili Date: Wed, 27 Feb 2019 15:54:13 +0100 Subject: [PATCH 1/8] fixed asymmetrical equality of cyclic objects (#7730) * fixed assymetrical equality of cyclic objects * updated changelog * added more test cases and changed algorithm * reverted to the old algorith and added additional check * removed unnecessary check * added more assertions to check for symmetrical equality --- CHANGELOG.md | 1 + .../expect/src/__tests__/matchers.test.js | 53 +++++++++++++++++++ packages/expect/src/jasmineUtils.ts | 17 +++--- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe3a1932a876..361a0877ccf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,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/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. From 438a178c8addf2806bebf44726d080921ad9984f Mon Sep 17 00:00:00 2001 From: Alcedo Nathaniel De Guzman Jr Date: Thu, 28 Feb 2019 01:40:14 +0800 Subject: [PATCH 2/8] [WIP] Migrate jest-repl to typescript (#8000) --- CHANGELOG.md | 1 + packages/jest-repl/package.json | 3 ++ .../jest-repl/src/cli/{args.js => args.ts} | 6 ++-- .../jest-repl/src/cli/{index.js => index.ts} | 12 +++---- .../jest-repl/src/cli/{repl.js => repl.ts} | 34 +++++++++++-------- packages/jest-repl/tsconfig.json | 15 ++++++++ 6 files changed, 46 insertions(+), 25 deletions(-) rename packages/jest-repl/src/cli/{args.js => args.ts} (88%) rename packages/jest-repl/src/cli/{index.js => index.ts} (74%) rename packages/jest-repl/src/cli/{repl.js => repl.ts} (70%) create mode 100644 packages/jest-repl/tsconfig.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 361a0877ccf7..d68df0efdee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,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)) ### Performance 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"} + ] +} From df3eb5e8269db403b72baa62a04691a09b3ea8ce Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Wed, 27 Feb 2019 21:20:47 -0500 Subject: [PATCH 3/8] feat: add "skipPackageJson" option (#7778) --- CHANGELOG.md | 1 + packages/jest-haste-map/src/index.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d68df0efdee3..d26f7237dbde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `[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 ### Fixes diff --git a/packages/jest-haste-map/src/index.ts b/packages/jest-haste-map/src/index.ts index fedc0f81a019..30e62ab773dd 100644 --- a/packages/jest-haste-map/src/index.ts +++ b/packages/jest-haste-map/src/index.ts @@ -64,6 +64,7 @@ type Options = { retainAllFiles: boolean; rootDir: string; roots: Array; + skipPackageJson?: boolean; throwOnModuleCollision?: boolean; useWatchman?: boolean; watch?: boolean; @@ -87,6 +88,7 @@ type InternalOptions = { retainAllFiles: boolean; rootDir: string; roots: Array; + skipPackageJson: boolean; throwOnModuleCollision: boolean; useWatchman: boolean; watch: boolean; @@ -108,6 +110,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`. @@ -256,6 +259,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, @@ -623,6 +627,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, From 5d2d46f1597e2880aeca4126947d7390cc6ff827 Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Thu, 28 Feb 2019 14:02:41 +0000 Subject: [PATCH 4/8] [jest-get-type] Add isPrimitive function (#7708) --- CHANGELOG.md | 3 ++- packages/jest-each/src/bind.js | 14 +++-------- .../{index.test.ts => getType.test.ts} | 2 +- .../src/__tests__/isPrimitive.test.ts | 25 +++++++++++++++++++ packages/jest-get-type/src/index.ts | 15 +++++++++-- 5 files changed, 44 insertions(+), 15 deletions(-) rename packages/jest-get-type/src/__tests__/{index.test.ts => getType.test.ts} (97%) create mode 100644 packages/jest-get-type/src/__tests__/isPrimitive.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d26f7237dbde..6d0474da0917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +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 +- `[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 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-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; From 1b2fffa9f13a67e54441606fda75822064cee57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Jim=C3=A9nez=20Es=C3=BAn?= Date: Thu, 28 Feb 2019 18:09:36 +0100 Subject: [PATCH 5/8] Simplify and enforce duplicate haste map errors (#8002) --- CHANGELOG.md | 1 + .../__snapshots__/index.test.js.snap | 32 +++------ .../src/__tests__/index.test.js | 17 ++++- packages/jest-haste-map/src/index.ts | 70 +++++++++++++------ .../__tests__/runtime_require_mock.test.js | 25 ------- 5 files changed, 73 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0474da0917..e58555d5bc52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - `[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 ### Chore & Maintenance 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 30e62ab773dd..e4410350efa9 100644 --- a/packages/jest-haste-map/src/index.ts +++ b/packages/jest-haste-map/src/index.ts @@ -436,27 +436,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 +561,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); } @@ -1079,9 +1091,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); } @@ -1091,6 +1116,7 @@ function copyMap(input: Map): Map { } HasteMap.H = H; +HasteMap.DuplicateError = DuplicateError; HasteMap.ModuleMap = HasteModuleMap; export = HasteMap; 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 From 591eb990a4f4eca2e746ac137c38574ee63e6735 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 28 Feb 2019 19:09:04 +0100 Subject: [PATCH 6/8] Fix missing error after test (#8005) --- CHANGELOG.md | 2 + e2e/__tests__/failures.test.ts | 13 +- .../__tests__/errorAfterTestComplete.test.js | 14 ++ packages/jest-circus/src/utils.ts | 137 ++++++++++-------- packages/jest-jasmine2/src/jasmine/Env.js | 15 +- 5 files changed, 113 insertions(+), 68 deletions(-) create mode 100644 e2e/failures/__tests__/errorAfterTestComplete.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e58555d5bc52..1bef5ad88fba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ - `[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 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/jest-circus/src/utils.ts b/packages/jest-circus/src/utils.ts index 7917d1356e9d..19e115baec86 100644 --- a/packages/jest-circus/src/utils.ts +++ b/packages/jest-circus/src/utils.ts @@ -153,84 +153,93 @@ const _makeTimeoutMessage = (timeout: number, isHook: boolean) => // the original values in the variables before we require any files. const {setTimeout, clearTimeout} = global; -export const callAsyncCircusFn = ( +function checkIsError(error: any): error is Error { + return !!(error && (error as Error).message && (error as Error).stack); +} + +export const callAsyncCircusFn = async ( fn: AsyncFn, testContext: TestContext | undefined, {isHook, timeout}: {isHook?: boolean | null; timeout: number}, ): Promise => { - let timeoutID: NodeJS.Timeout; - - return new Promise((resolve, reject) => { - timeoutID = setTimeout( - () => reject(_makeTimeoutMessage(timeout, !!isHook)), - timeout, - ); - - // If this fn accepts `done` callback we return a promise that fulfills as - // 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(); - }; - - return fn.call(testContext, done); - } + let timeoutID: NodeJS.Timeout | undefined; + let completed = false; + + try { + return await new Promise((resolve, reject) => { + timeoutID = setTimeout( + () => reject(_makeTimeoutMessage(timeout, !!isHook)), + timeout, + ); + + // If this fn accepts `done` callback we return a promise that fulfills as + // soon as `done` called. + if (fn.length) { + const done = (reason?: Error | string): void => { + 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; + } - let returnedValue; - if (isGeneratorFn(fn)) { - returnedValue = co.wrap(fn).call({}); - } else { - try { - returnedValue = fn.call(testContext); - } catch (error) { - return reject(error); + return reason ? reject(errorAsErrorObject) : resolve(); + }; + + return fn.call(testContext, done); } - } - // If it's a Promise, return it. Test for an object with a `then` function - // to support custom Promise implementations. - if ( - typeof returnedValue === 'object' && - returnedValue !== null && - typeof returnedValue.then === 'function' - ) { - return returnedValue.then(resolve, reject); - } + let returnedValue; + if (isGeneratorFn(fn)) { + returnedValue = co.wrap(fn).call({}); + } else { + try { + returnedValue = fn.call(testContext); + } catch (error) { + return reject(error); + } + } + + // If it's a Promise, return it. Test for an object with a `then` function + // to support custom Promise implementations. + if ( + typeof returnedValue === 'object' && + returnedValue !== null && + typeof returnedValue.then === 'function' + ) { + return returnedValue.then(resolve, reject); + } - if (!isHook && returnedValue !== void 0) { - return reject( - new Error( - ` + if (!isHook && returnedValue !== void 0) { + return reject( + new Error( + ` test functions can only return Promise or undefined. Returned value: ${String(returnedValue)} `, - ), - ); - } + ), + ); + } - // Otherwise this test is synchronous, and if it didn't throw it means - // it passed. - return resolve(); - }) - .then(() => { - // If timeout is not cleared/unrefed the node process won't exit until - // it's resolved. - timeoutID.unref && timeoutID.unref(); - clearTimeout(timeoutID); - }) - .catch(error => { + // Otherwise this test is synchronous, and if it didn't throw it means + // it passed. + return resolve(); + }); + } finally { + completed = true; + // If timeout is not cleared/unrefed the node process won't exit until + // it's resolved. + if (timeoutID) { timeoutID.unref && timeoutID.unref(); clearTimeout(timeoutID); - throw error; - }); + } + } }; export const getTestDuration = (test: TestEntry): number | null => { 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, }); }; } From cb2630b5085bb0ae909062b80807209824dfc495 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Thu, 28 Feb 2019 20:21:03 +0100 Subject: [PATCH 7/8] chore: fix broken test in circus (#8009) --- e2e/__tests__/detectOpenHandles.ts | 2 +- packages/jest-circus/src/utils.ts | 133 +++++++++++++++-------------- 2 files changed, 69 insertions(+), 66 deletions(-) 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/packages/jest-circus/src/utils.ts b/packages/jest-circus/src/utils.ts index 19e115baec86..fbad7a725c77 100644 --- a/packages/jest-circus/src/utils.ts +++ b/packages/jest-circus/src/utils.ts @@ -157,89 +157,92 @@ function checkIsError(error: any): error is Error { return !!(error && (error as Error).message && (error as Error).stack); } -export const callAsyncCircusFn = async ( +export const callAsyncCircusFn = ( fn: AsyncFn, testContext: TestContext | undefined, {isHook, timeout}: {isHook?: boolean | null; timeout: number}, ): Promise => { - let timeoutID: NodeJS.Timeout | undefined; + let timeoutID: NodeJS.Timeout; let completed = false; - try { - return await new Promise((resolve, reject) => { - timeoutID = setTimeout( - () => reject(_makeTimeoutMessage(timeout, !!isHook)), - timeout, - ); - - // If this fn accepts `done` callback we return a promise that fulfills as - // soon as `done` called. - if (fn.length) { - const done = (reason?: Error | string): void => { - 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 new Promise((resolve, reject) => { + timeoutID = setTimeout( + () => reject(_makeTimeoutMessage(timeout, !!isHook)), + timeout, + ); + + // If this fn accepts `done` callback we return a promise that fulfills as + // soon as `done` called. + if (fn.length) { + const done = (reason?: Error | string): void => { + 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 reason ? reject(errorAsErrorObject) : resolve(); + }; - return fn.call(testContext, done); - } + return fn.call(testContext, done); + } - let returnedValue; - if (isGeneratorFn(fn)) { - returnedValue = co.wrap(fn).call({}); - } else { - try { - returnedValue = fn.call(testContext); - } catch (error) { - return reject(error); - } + let returnedValue; + if (isGeneratorFn(fn)) { + returnedValue = co.wrap(fn).call({}); + } else { + try { + returnedValue = fn.call(testContext); + } catch (error) { + return reject(error); } + } - // If it's a Promise, return it. Test for an object with a `then` function - // to support custom Promise implementations. - if ( - typeof returnedValue === 'object' && - returnedValue !== null && - typeof returnedValue.then === 'function' - ) { - return returnedValue.then(resolve, reject); - } + // If it's a Promise, return it. Test for an object with a `then` function + // to support custom Promise implementations. + if ( + typeof returnedValue === 'object' && + returnedValue !== null && + typeof returnedValue.then === 'function' + ) { + return returnedValue.then(resolve, reject); + } - if (!isHook && returnedValue !== void 0) { - return reject( - new Error( - ` + if (!isHook && returnedValue !== void 0) { + return reject( + new Error( + ` test functions can only return Promise or undefined. Returned value: ${String(returnedValue)} `, - ), - ); - } + ), + ); + } - // Otherwise this test is synchronous, and if it didn't throw it means - // it passed. - return resolve(); - }); - } finally { - completed = true; - // If timeout is not cleared/unrefed the node process won't exit until - // it's resolved. - if (timeoutID) { + // Otherwise this test is synchronous, and if it didn't throw it means + // it passed. + 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; + }); }; export const getTestDuration = (test: TestEntry): number | null => { From de52b481284f7a0e5cba6b1f81d3b5ff8dc261e7 Mon Sep 17 00:00:00 2001 From: Mikael Lirbank Date: Thu, 28 Feb 2019 11:21:48 -0800 Subject: [PATCH 8/8] chore: migrate jest-environment-jsdom to TypeScript (#8003) --- CHANGELOG.md | 1 + packages/jest-config/tsconfig.json | 6 +- packages/jest-environment-jsdom/package.json | 5 +- .../src/__mocks__/{index.js => index.ts} | 9 +- ...ment.test.js => jsdom_environment.test.ts} | 0 .../src/{index.js => index.ts} | 89 ++++++++++++------- packages/jest-environment-jsdom/tsconfig.json | 14 +++ 7 files changed, 84 insertions(+), 40 deletions(-) rename packages/jest-environment-jsdom/src/__mocks__/{index.js => index.ts} (81%) rename packages/jest-environment-jsdom/src/__tests__/{jsdom_environment.test.js => jsdom_environment.test.ts} (100%) rename packages/jest-environment-jsdom/src/{index.js => index.ts} (55%) create mode 100644 packages/jest-environment-jsdom/tsconfig.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bef5ad88fba..f7e612711dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ ### 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)) 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-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"} + ] +}