diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ebdec78cb0c..5dcbe823063a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ - `[jest-test-typescript-parser]` Remove from the repository ([#7232](https://github.com/facebook/jest/pull/7232)) - `[tests]` Free tests from the dependency on value of FORCE_COLOR ([#6585](https://github.com/facebook/jest/pull/6585/files)) - `[jest-diff]` Standardize filenames ([#7238](https://github.com/facebook/jest/pull/7238)) +- `[*]` Add babel plugin to make sure Jest is unaffected by fake Promise implementations ([#7225](https://github.com/facebook/jest/pull/7225)) ### Performance diff --git a/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js b/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js index 34108cb12307..9ceed2811e66 100644 --- a/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js +++ b/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js @@ -7,9 +7,9 @@ * @flow */ -import type {TestResult, Status} from 'types/TestResult'; +import type {AssertionResult, TestResult, Status} from 'types/TestResult'; import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; -import type {Event, TestEntry} from 'types/Circus'; +import type {Event, RunResult, TestEntry} from 'types/Circus'; import {extractExpectedAssertionsErrors, getState, setState} from 'expect'; import {formatExecError, formatResultsErrors} from 'jest-message-util'; @@ -19,12 +19,11 @@ import { buildSnapshotResolver, } from 'jest-snapshot'; import {addEventHandler, dispatch, ROOT_DESCRIBE_BLOCK_NAME} from '../state'; -import {getTestID, getOriginalPromise} from '../utils'; +import {getTestID} from '../utils'; import run from '../run'; // eslint-disable-next-line import/default import globals from '../index'; -const Promise = getOriginalPromise(); export const initialize = ({ config, getPrettier, @@ -123,46 +122,48 @@ export const runAndTransformResultsToJestFormat = async ({ globalConfig: GlobalConfig, testPath: string, }): Promise => { - const runResult = await run(); + const runResult: RunResult = await run(); let numFailingTests = 0; let numPassingTests = 0; let numPendingTests = 0; let numTodoTests = 0; - const assertionResults = runResult.testResults.map(testResult => { - let status: Status; - if (testResult.status === 'skip') { - status = 'pending'; - numPendingTests += 1; - } else if (testResult.status === 'todo') { - status = 'todo'; - numTodoTests += 1; - } else if (testResult.errors.length) { - status = 'failed'; - numFailingTests += 1; - } else { - status = 'passed'; - numPassingTests += 1; - } - - const ancestorTitles = testResult.testPath.filter( - name => name !== ROOT_DESCRIBE_BLOCK_NAME, - ); - const title = ancestorTitles.pop(); - - return { - ancestorTitles, - duration: testResult.duration, - failureMessages: testResult.errors, - fullName: ancestorTitles.concat(title).join(' '), - invocations: testResult.invocations, - location: testResult.location, - numPassingAsserts: 0, - status, - title: testResult.testPath[testResult.testPath.length - 1], - }; - }); + const assertionResults: Array = runResult.testResults.map( + testResult => { + let status: Status; + if (testResult.status === 'skip') { + status = 'pending'; + numPendingTests += 1; + } else if (testResult.status === 'todo') { + status = 'todo'; + numTodoTests += 1; + } else if (testResult.errors.length) { + status = 'failed'; + numFailingTests += 1; + } else { + status = 'passed'; + numPassingTests += 1; + } + + const ancestorTitles = testResult.testPath.filter( + name => name !== ROOT_DESCRIBE_BLOCK_NAME, + ); + const title = ancestorTitles.pop(); + + return { + ancestorTitles, + duration: testResult.duration, + failureMessages: testResult.errors, + fullName: ancestorTitles.concat(title).join(' '), + invocations: testResult.invocations, + location: testResult.location, + numPassingAsserts: 0, + status, + title: testResult.testPath[testResult.testPath.length - 1], + }; + }, + ); let failureMessage = formatResultsErrors( assertionResults, diff --git a/packages/jest-circus/src/run.js b/packages/jest-circus/src/run.js index 37fcfc8d1616..507dfc441193 100644 --- a/packages/jest-circus/src/run.js +++ b/packages/jest-circus/src/run.js @@ -23,11 +23,8 @@ import { getTestID, invariant, makeRunResult, - getOriginalPromise, } from './utils'; -const Promise = getOriginalPromise(); - const run = async (): Promise => { const {rootDescribeBlock} = getState(); dispatch({name: 'run_start'}); diff --git a/packages/jest-circus/src/utils.js b/packages/jest-circus/src/utils.js index 2f0a44af3c37..96da54744ecf 100644 --- a/packages/jest-circus/src/utils.js +++ b/packages/jest-circus/src/utils.js @@ -32,12 +32,6 @@ import prettyFormat from 'pretty-format'; import {getState} from './state'; -// Try getting the real promise object from the context, if available. Someone -// could have overridden it in a test. Async functions return it implicitly. -// eslint-disable-next-line no-unused-vars -const Promise = global[Symbol.for('jest-native-promise')] || global.Promise; -export const getOriginalPromise = () => Promise; - const stackUtils = new StackUtils({cwd: 'A path that does not exist'}); export const makeDescribe = ( diff --git a/packages/jest-jasmine2/src/jasmine/Env.js b/packages/jest-jasmine2/src/jasmine/Env.js index 0e57d1f1968b..21d27d753c23 100644 --- a/packages/jest-jasmine2/src/jasmine/Env.js +++ b/packages/jest-jasmine2/src/jasmine/Env.js @@ -37,11 +37,6 @@ import checkIsError from '../is_error'; import assertionErrorMessage from '../assert_support'; import {ErrorWithStack} from 'jest-util'; -// Try getting the real promise object from the context, if available. Someone -// could have overridden it in a test. Async functions return it implicitly. -// eslint-disable-next-line no-unused-vars -const Promise = global[Symbol.for('jest-native-promise')] || global.Promise; - export default function(j$) { function Env(options) { options = options || {}; diff --git a/packages/jest-jasmine2/src/p_cancelable.js b/packages/jest-jasmine2/src/p_cancelable.js index 1b54f624c6d7..4af085802444 100644 --- a/packages/jest-jasmine2/src/p_cancelable.js +++ b/packages/jest-jasmine2/src/p_cancelable.js @@ -2,10 +2,6 @@ 'use strict'; -// Try getting the real promise object from the context, if available. Someone -// could have overridden it in a test. -const Promise = global[Symbol.for('jest-native-promise')] || global.Promise; - class CancelError extends Error { constructor() { super('Promise was canceled'); diff --git a/packages/jest-jasmine2/src/p_timeout.js b/packages/jest-jasmine2/src/p_timeout.js index 99c217ea5ce7..f255f3440a5a 100644 --- a/packages/jest-jasmine2/src/p_timeout.js +++ b/packages/jest-jasmine2/src/p_timeout.js @@ -7,10 +7,6 @@ * @flow */ -// Try getting the real promise object from the context, if available. Someone -// could have overridden it in a test. -const Promise = global[Symbol.for('jest-native-promise')] || global.Promise; - // A specialized version of `p-timeout` that does not touch globals. // It does not throw on timeout. export default function pTimeout( diff --git a/packages/jest-jasmine2/src/queue_runner.js b/packages/jest-jasmine2/src/queue_runner.js index d72ec1bfabbd..280a1ff73edd 100644 --- a/packages/jest-jasmine2/src/queue_runner.js +++ b/packages/jest-jasmine2/src/queue_runner.js @@ -7,11 +7,6 @@ * @flow */ -// Try getting the real promise object from the context, if available. Someone -// could have overridden it in a test. -const Promise: Class = - global[Symbol.for('jest-native-promise')] || global.Promise; - import PCancelable from './p_cancelable'; import pTimeout from './p_timeout'; @@ -27,6 +22,7 @@ type Options = { type QueueableFn = { fn: (next: () => void) => void, timeout?: () => number, + initError?: Error, }; export default function queueRunner(options: Options) { @@ -34,7 +30,7 @@ export default function queueRunner(options: Options) { onCancel(resolve); }); - const mapper = ({fn, timeout, initError = new Error()}) => { + const mapper = ({fn, timeout, initError = new Error()}: QueueableFn) => { let promise = new Promise(resolve => { const next = function(err) { if (err) { diff --git a/packages/jest-jasmine2/src/reporter.js b/packages/jest-jasmine2/src/reporter.js index 2a867736f29a..0b482c44fca5 100644 --- a/packages/jest-jasmine2/src/reporter.js +++ b/packages/jest-jasmine2/src/reporter.js @@ -16,10 +16,6 @@ import type { TestResult, } from 'types/TestResult'; -// Try getting the real promise object from the context, if available. Someone -// could have overridden it in a test. -const Promise = global[Symbol.for('jest-native-promise')] || global.Promise; - import {formatResultsErrors} from 'jest-message-util'; type Suite = { diff --git a/packages/jest-jasmine2/src/tree_processor.js b/packages/jest-jasmine2/src/tree_processor.js index 1245ee7a4494..6cf093c1787b 100644 --- a/packages/jest-jasmine2/src/tree_processor.js +++ b/packages/jest-jasmine2/src/tree_processor.js @@ -26,11 +26,6 @@ type TreeNode = { children?: Array, }; -// Try getting the real promise object from the context, if available. Someone -// could have overridden it in a test. Async functions return it implicitly. -// eslint-disable-next-line no-unused-vars -const Promise = global[Symbol.for('jest-native-promise')] || global.Promise; - export default function treeProcessor(options: Options) { const { nodeComplete, diff --git a/scripts/babel-plugin-jest-native-promise.js b/scripts/babel-plugin-jest-native-promise.js new file mode 100644 index 000000000000..b0fa9e57db85 --- /dev/null +++ b/scripts/babel-plugin-jest-native-promise.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + */ + +'use strict'; + +// This plugin exists to make sure that we use a `Promise` that has not been messed with by user code. +// Might consider extending this to other globals as well in the future + +module.exports = ({template}) => { + const promiseDeclaration = template(` + var Promise = global[Symbol.for('jest-native-promise')] || global.Promise; + `); + + return { + name: 'jest-native-promise', + visitor: { + ReferencedIdentifier(path, state) { + if (path.node.name === 'Promise' && !state.injectedPromise) { + state.injectedPromise = true; + path + .findParent(p => p.isProgram()) + .unshiftContainer('body', promiseDeclaration()); + } + }, + }, + }; +}; diff --git a/scripts/build.js b/scripts/build.js index ebaf99bca2ba..07af2abdf4b6 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -150,7 +150,13 @@ function buildFile(file, silent) { const options = Object.assign({}, transformOptions); options.plugins = options.plugins.slice(); - if (!INLINE_REQUIRE_BLACKLIST.test(file)) { + if (INLINE_REQUIRE_BLACKLIST.test(file)) { + // The modules in the blacklist are injected into the user's sandbox + // We need to guard `Promise` there. + options.plugins.push( + require.resolve('./babel-plugin-jest-native-promise') + ); + } else { // Remove normal plugin. options.plugins = options.plugins.filter( plugin => diff --git a/types/Circus.js b/types/Circus.js index 7a95671f9460..ae13529ed043 100644 --- a/types/Circus.js +++ b/types/Circus.js @@ -9,7 +9,7 @@ export type DoneFn = (reason?: string | Error) => void; export type BlockFn = () => void; -export type BlockName = string | Function; +export type BlockName = string; export type BlockMode = void | 'skip' | 'only' | 'todo'; export type TestMode = BlockMode; export type TestName = string; @@ -153,6 +153,7 @@ export type TestStatus = 'skip' | 'done' | 'todo'; export type TestResult = {| duration: ?number, errors: Array, + invocations: number, status: TestStatus, location: ?{|column: number, line: number|}, testPath: Array, diff --git a/types/TestResult.js b/types/TestResult.js index 928dc0d280e8..7ef862e2308e 100644 --- a/types/TestResult.js +++ b/types/TestResult.js @@ -98,6 +98,7 @@ export type AssertionResult = {| duration?: ?Milliseconds, failureMessages: Array, fullName: string, + invocations?: number, location: ?Callsite, numPassingAsserts: number, status: Status,