From e998c9230cb78b3befe0b1b57b36fd5353e766f0 Mon Sep 17 00:00:00 2001 From: doniyor2109 Date: Sat, 23 Feb 2019 02:56:44 +0500 Subject: [PATCH] TS migration `jest-circus` (#7916) * Migration to ts (part 1) * Migration to ts (part 2) * Migration to ts (part 3) * Minor tweaks * TS migration (part 4) * Wrong dts * dts for co * Added project references * Remove not ts module * Replace custom co dts with @types/co * No index file Co-Authored-By: doniyor2109 * Some tweaks * Some tweaks * Temp DiffOptions type * Remove extra eslint disable * Workaround for global vars * Resolves * Move @types/co to devDeps * Update packages/jest-circus/src/run.ts Co-Authored-By: doniyor2109 * Update packages/jest-circus/src/utils.ts Co-Authored-By: doniyor2109 * Tweaks * Remove extra types * Fix failing test Cannot run ts via node cli * TS migration part (4) - Added Global and Environment types - Workaround for RawMatcherFn type * Fix linter errors * Fix types for tests * Fix @jest/types cannot be found in test * Detailed comment for flowfix * Ignore ts errors for non migrated modules * `import =` is not supported by @babel/plugin-transform-typescript * Fix weired ts error Exported variable 'jestAdapter' has or is using name '$JestEnvironment' from external module "packages/jest-types/build/Environment" but cannot be named. https://github.com/Microsoft/TypeScript/issues/5711 * Fix linter errors * Remove extra eslint disables * Move expect types to @jest/types * tweaks * remove jest config change * fix test * Added reminder for replacing Each type * keep comments from old tests * Update CHANGELOG.md --- CHANGELOG.md | 1 + packages/jest-circus/package.json | 4 + ...estEventHandler.js => testEventHandler.ts} | 6 +- .../__mocks__/{testUtils.js => testUtils.ts} | 20 +- ...All.test.js.snap => afterAll.test.ts.snap} | 0 ...est.test.js.snap => baseTest.test.ts.snap} | 0 ...{hooks.test.js.snap => hooks.test.ts.snap} | 0 .../{afterAll.test.js => afterAll.test.ts} | 4 - .../{baseTest.test.js => baseTest.test.ts} | 4 - ...rror.test.js => circusItTestError.test.ts} | 22 +- ....test.js => circusItTodoTestError.test.ts} | 12 +- .../{hooks.test.js => hooks.test.ts} | 4 - ...{hooksError.test.js => hooksError.test.ts} | 11 +- .../src/{eventHandler.js => eventHandler.ts} | 18 +- ...ertErrors.js => formatNodeAssertErrors.ts} | 48 ++-- ...rrorHandlers.js => globalErrorHandlers.ts} | 6 +- packages/jest-circus/src/index.js | 151 ----------- packages/jest-circus/src/index.ts | 172 +++++++++++++ .../{jestAdapter.js => jestAdapter.ts} | 28 +- ...{jestAdapterInit.js => jestAdapterInit.ts} | 157 +++++------ .../{jestExpect.js => jestExpect.ts} | 12 +- packages/jest-circus/src/{run.js => run.ts} | 21 +- .../jest-circus/src/{state.js => state.ts} | 5 +- packages/jest-circus/src/types.ts | 243 ++++++++++++++++++ .../jest-circus/src/{utils.js => utils.ts} | 136 +++++----- packages/jest-circus/tsconfig.json | 8 + packages/jest-types/src/Environment.ts | 44 ++++ packages/jest-types/src/Global.ts | 75 ++++++ packages/jest-types/src/index.ts | 4 + yarn.lock | 5 + 30 files changed, 799 insertions(+), 422 deletions(-) rename packages/jest-circus/src/__mocks__/{testEventHandler.js => testEventHandler.ts} (94%) rename packages/jest-circus/src/__mocks__/{testUtils.js => testUtils.ts} (85%) rename packages/jest-circus/src/__tests__/__snapshots__/{afterAll.test.js.snap => afterAll.test.ts.snap} (100%) rename packages/jest-circus/src/__tests__/__snapshots__/{baseTest.test.js.snap => baseTest.test.ts.snap} (100%) rename packages/jest-circus/src/__tests__/__snapshots__/{hooks.test.js.snap => hooks.test.ts.snap} (100%) rename packages/jest-circus/src/__tests__/{afterAll.test.js => afterAll.test.ts} (97%) rename packages/jest-circus/src/__tests__/{baseTest.test.js => baseTest.test.ts} (95%) rename packages/jest-circus/src/__tests__/{circusItTestError.test.js => circusItTestError.test.ts} (82%) rename packages/jest-circus/src/__tests__/{circusItTodoTestError.test.js => circusItTodoTestError.test.ts} (84%) rename packages/jest-circus/src/__tests__/{hooks.test.js => hooks.test.ts} (98%) rename packages/jest-circus/src/__tests__/{hooksError.test.js => hooksError.test.ts} (81%) rename packages/jest-circus/src/{eventHandler.js => eventHandler.ts} (92%) rename packages/jest-circus/src/{formatNodeAssertErrors.js => formatNodeAssertErrors.ts} (81%) rename packages/jest-circus/src/{globalErrorHandlers.js => globalErrorHandlers.ts} (94%) delete mode 100644 packages/jest-circus/src/index.js create mode 100644 packages/jest-circus/src/index.ts rename packages/jest-circus/src/legacy-code-todo-rewrite/{jestAdapter.js => jestAdapter.ts} (81%) rename packages/jest-circus/src/legacy-code-todo-rewrite/{jestAdapterInit.js => jestAdapterInit.ts} (61%) rename packages/jest-circus/src/legacy-code-todo-rewrite/{jestExpect.js => jestExpect.ts} (73%) rename packages/jest-circus/src/{run.js => run.ts} (92%) rename packages/jest-circus/src/{state.js => state.ts} (91%) create mode 100644 packages/jest-circus/src/types.ts rename packages/jest-circus/src/{utils.js => utils.ts} (77%) create mode 100644 packages/jest-circus/tsconfig.json create mode 100644 packages/jest-types/src/Environment.ts create mode 100644 packages/jest-types/src/Global.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 532347008eaa..aa078a389e3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ - `[docs]` Add missing import to docs ([#7928](https://github.com/facebook/jest/pull/7928)) - `[jest-resolve-dependencies]`: Migrate to TypeScript ([#7922](https://github.com/facebook/jest/pull/7922)) - `[expect]`: Migrate to TypeScript ([#7919](https://github.com/facebook/jest/pull/7919)) +- `[jest-circus]`: Migrate to TypeScript ([#7916](https://github.com/facebook/jest/pull/7916)) ### Performance diff --git a/packages/jest-circus/package.json b/packages/jest-circus/package.json index 7ce23e74d4a0..1986ebe211cf 100644 --- a/packages/jest-circus/package.json +++ b/packages/jest-circus/package.json @@ -8,8 +8,11 @@ }, "license": "MIT", "main": "build/index.js", + "types": "build/index.d.ts", "dependencies": { "@babel/traverse": "^7.1.0", + "@jest/types": "^24.1.0", + "@types/node": "*", "chalk": "^2.0.1", "co": "^4.6.0", "expect": "^24.1.0", @@ -25,6 +28,7 @@ }, "devDependencies": { "@types/babel__traverse": "^7.0.4", + "@types/co": "^4.6.0", "@types/stack-utils": "^1.0.1", "execa": "^1.0.0", "jest-diff": "^24.0.0", diff --git a/packages/jest-circus/src/__mocks__/testEventHandler.js b/packages/jest-circus/src/__mocks__/testEventHandler.ts similarity index 94% rename from packages/jest-circus/src/__mocks__/testEventHandler.js rename to packages/jest-circus/src/__mocks__/testEventHandler.ts index 7455a4fcc42a..1bc877b075f9 100644 --- a/packages/jest-circus/src/__mocks__/testEventHandler.js +++ b/packages/jest-circus/src/__mocks__/testEventHandler.ts @@ -4,13 +4,9 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow scrict-local */ -'use strict'; - -import type {EventHandler} from 'types/Circus'; +import {EventHandler} from '../types'; const testEventHandler: EventHandler = (event, state) => { switch (event.name) { diff --git a/packages/jest-circus/src/__mocks__/testUtils.js b/packages/jest-circus/src/__mocks__/testUtils.ts similarity index 85% rename from packages/jest-circus/src/__mocks__/testUtils.js rename to packages/jest-circus/src/__mocks__/testUtils.ts index fd71d097c5f5..6a53bb586938 100644 --- a/packages/jest-circus/src/__mocks__/testUtils.js +++ b/packages/jest-circus/src/__mocks__/testUtils.ts @@ -4,20 +4,17 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow strict-local */ -'use strict'; -// $FlowFixMe - execa is untyped -import {sync as spawnSync} from 'execa'; import fs from 'fs'; import os from 'os'; import path from 'path'; import crypto from 'crypto'; +import {sync as spawnSync, ExecaReturns} from 'execa'; +// @ts-ignore import {skipSuiteOnWindows} from '../../../../scripts/ConditionalTest'; -const CIRCUS_PATH = require.resolve('../../build/index'); +const CIRCUS_PATH = require.resolve('../../build'); const CIRCUS_RUN_PATH = require.resolve('../../build/run'); const CIRCUS_STATE_PATH = require.resolve('../../build/state'); const TEST_EVENT_HANDLER_PATH = require.resolve('./testEventHandler'); @@ -25,6 +22,11 @@ const BABEL_REGISTER_PATH = require.resolve('@babel/register'); skipSuiteOnWindows(); +interface Result extends ExecaReturns { + status: number; + error: string; +} + export const runTest = (source: string) => { const filename = crypto .createHash('md5') @@ -33,7 +35,7 @@ export const runTest = (source: string) => { const tmpFilename = path.join(os.tmpdir(), filename); const content = ` - require('${BABEL_REGISTER_PATH}'); + require('${BABEL_REGISTER_PATH}')({extensions: [".js", ".ts"]}); const circus = require('${CIRCUS_PATH}'); global.test = circus.test; global.describe = circus.describe; @@ -54,7 +56,9 @@ export const runTest = (source: string) => { `; fs.writeFileSync(tmpFilename, content); - const result = spawnSync('node', [tmpFilename], {cwd: process.cwd()}); + const result = spawnSync('node', [tmpFilename], { + cwd: process.cwd(), + }) as Result; // For compat with cross-spawn result.status = result.code; diff --git a/packages/jest-circus/src/__tests__/__snapshots__/afterAll.test.js.snap b/packages/jest-circus/src/__tests__/__snapshots__/afterAll.test.ts.snap similarity index 100% rename from packages/jest-circus/src/__tests__/__snapshots__/afterAll.test.js.snap rename to packages/jest-circus/src/__tests__/__snapshots__/afterAll.test.ts.snap diff --git a/packages/jest-circus/src/__tests__/__snapshots__/baseTest.test.js.snap b/packages/jest-circus/src/__tests__/__snapshots__/baseTest.test.ts.snap similarity index 100% rename from packages/jest-circus/src/__tests__/__snapshots__/baseTest.test.js.snap rename to packages/jest-circus/src/__tests__/__snapshots__/baseTest.test.ts.snap diff --git a/packages/jest-circus/src/__tests__/__snapshots__/hooks.test.js.snap b/packages/jest-circus/src/__tests__/__snapshots__/hooks.test.ts.snap similarity index 100% rename from packages/jest-circus/src/__tests__/__snapshots__/hooks.test.js.snap rename to packages/jest-circus/src/__tests__/__snapshots__/hooks.test.ts.snap diff --git a/packages/jest-circus/src/__tests__/afterAll.test.js b/packages/jest-circus/src/__tests__/afterAll.test.ts similarity index 97% rename from packages/jest-circus/src/__tests__/afterAll.test.js rename to packages/jest-circus/src/__tests__/afterAll.test.ts index a4eacd2e6ec1..386e6263e3ed 100644 --- a/packages/jest-circus/src/__tests__/afterAll.test.js +++ b/packages/jest-circus/src/__tests__/afterAll.test.ts @@ -4,12 +4,8 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow strict-local */ -'use strict'; - import {runTest} from '../__mocks__/testUtils'; test('tests are not marked done until their parent afterAll runs', () => { diff --git a/packages/jest-circus/src/__tests__/baseTest.test.js b/packages/jest-circus/src/__tests__/baseTest.test.ts similarity index 95% rename from packages/jest-circus/src/__tests__/baseTest.test.js rename to packages/jest-circus/src/__tests__/baseTest.test.ts index f8bbb457e84c..66d743f5c434 100644 --- a/packages/jest-circus/src/__tests__/baseTest.test.js +++ b/packages/jest-circus/src/__tests__/baseTest.test.ts @@ -4,12 +4,8 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow strict-local */ -'use strict'; - import {runTest} from '../__mocks__/testUtils'; test('simple test', () => { diff --git a/packages/jest-circus/src/__tests__/circusItTestError.test.js b/packages/jest-circus/src/__tests__/circusItTestError.test.ts similarity index 82% rename from packages/jest-circus/src/__tests__/circusItTestError.test.js rename to packages/jest-circus/src/__tests__/circusItTestError.test.ts index 4269b9c1bee3..05a4035decfb 100644 --- a/packages/jest-circus/src/__tests__/circusItTestError.test.js +++ b/packages/jest-circus/src/__tests__/circusItTestError.test.ts @@ -3,20 +3,18 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow strict-local */ -'use strict'; +import {Global} from '@jest/types'; -let circusIt; -let circusTest; +let circusIt: Global.It; +let circusTest: Global.It; // using jest-jasmine2's 'it' to test jest-circus's 'it'. Had to differentiate // the two with this alias. const aliasCircusIt = () => { - const {it, test} = require('../index.js'); + const {it, test} = require('../'); circusIt = it; circusTest = test; }; @@ -35,7 +33,7 @@ describe('test/it error throwing', () => { }); it(`it throws error with missing callback function`, () => { expect(() => { - // $FlowFixMe: Easy, we're testing runtime errors here + // @ts-ignore: Easy, we're testing runtime errors here circusIt('test2'); }).toThrowError( 'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.', @@ -43,13 +41,13 @@ describe('test/it error throwing', () => { }); it(`it throws an error when first argument isn't a string`, () => { expect(() => { - // $FlowFixMe: Easy, we're testing runtime errors here + // @ts-ignore: Easy, we're testing runtime errors here circusIt(() => {}); }).toThrowError('Invalid first argument, () => {}. It must be a string.'); }); it('it throws an error when callback function is not a function', () => { expect(() => { - // $FlowFixMe: Easy, we're testing runtime errors here + // @ts-ignore: Easy, we're testing runtime errors here circusIt('test4', 'test4b'); }).toThrowError( 'Invalid second argument, test4b. It must be a callback function.', @@ -62,7 +60,7 @@ describe('test/it error throwing', () => { }); it(`test throws error with missing callback function`, () => { expect(() => { - // $FlowFixMe: Easy, we're testing runtime errors here + // @ts-ignore: Easy, we're testing runtime errors here circusTest('test6'); }).toThrowError( 'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.', @@ -70,13 +68,13 @@ describe('test/it error throwing', () => { }); it(`test throws an error when first argument isn't a string`, () => { expect(() => { - // $FlowFixMe: Easy, we're testing runtime errors here + // @ts-ignore: Easy, we're testing runtime errors here circusTest(() => {}); }).toThrowError('Invalid first argument, () => {}. It must be a string.'); }); it('test throws an error when callback function is not a function', () => { expect(() => { - // $FlowFixMe: Easy, we're testing runtime errors here + // @ts-ignore: Easy, we're testing runtime errors here circusTest('test8', 'test8b'); }).toThrowError( 'Invalid second argument, test8b. It must be a callback function.', diff --git a/packages/jest-circus/src/__tests__/circusItTodoTestError.test.js b/packages/jest-circus/src/__tests__/circusItTodoTestError.test.ts similarity index 84% rename from packages/jest-circus/src/__tests__/circusItTodoTestError.test.js rename to packages/jest-circus/src/__tests__/circusItTodoTestError.test.ts index d7b017f6cc45..dfa31e2f9990 100644 --- a/packages/jest-circus/src/__tests__/circusItTodoTestError.test.js +++ b/packages/jest-circus/src/__tests__/circusItTodoTestError.test.ts @@ -3,19 +3,17 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow strict-local */ -'use strict'; +import {Global} from '@jest/types'; -let circusIt; +let circusIt: Global.It; // using jest-jasmine2's 'it' to test jest-circus's 'it'. Had to differentiate // the two with this alias. const aliasCircusIt = () => { - const {it} = require('../index.js'); + const {it} = require('../'); circusIt = it; }; @@ -24,7 +22,7 @@ aliasCircusIt(); describe('test/it.todo error throwing', () => { it('todo throws error when given no arguments', () => { expect(() => { - // $FlowFixMe: Testing runitme errors here + // @ts-ignore: Testing runtime errors here circusIt.todo(); }).toThrowError('Todo must be called with only a description.'); }); @@ -35,7 +33,7 @@ describe('test/it.todo error throwing', () => { }); it('todo throws error when given none string description', () => { expect(() => { - // $FlowFixMe: Testing runitme errors here + // @ts-ignore: Testing runtime errors here circusIt.todo(() => {}); }).toThrowError('Todo must be called with only a description.'); }); diff --git a/packages/jest-circus/src/__tests__/hooks.test.js b/packages/jest-circus/src/__tests__/hooks.test.ts similarity index 98% rename from packages/jest-circus/src/__tests__/hooks.test.js rename to packages/jest-circus/src/__tests__/hooks.test.ts index 52ce749949be..02b54ae8e515 100644 --- a/packages/jest-circus/src/__tests__/hooks.test.js +++ b/packages/jest-circus/src/__tests__/hooks.test.ts @@ -4,12 +4,8 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow strict-local */ -'use strict'; - import {runTest} from '../__mocks__/testUtils'; test('beforeEach is executed before each test in current/child describe blocks', () => { diff --git a/packages/jest-circus/src/__tests__/hooksError.test.js b/packages/jest-circus/src/__tests__/hooksError.test.ts similarity index 81% rename from packages/jest-circus/src/__tests__/hooksError.test.js rename to packages/jest-circus/src/__tests__/hooksError.test.ts index dcaac27a00be..5b39df11a43b 100644 --- a/packages/jest-circus/src/__tests__/hooksError.test.js +++ b/packages/jest-circus/src/__tests__/hooksError.test.ts @@ -3,17 +3,14 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow */ -'use strict'; - -const circus = require('../index.js'); +import circus from '../'; +import {HookType} from '../types'; -describe.each([['beforeEach'], ['beforeAll'], ['afterEach'], ['afterAll']])( +describe.each(['beforeEach', 'beforeAll', 'afterEach', 'afterAll'])( '%s hooks error throwing', - fn => { + (fn: HookType) => { test.each([ ['String'], [1], diff --git a/packages/jest-circus/src/eventHandler.js b/packages/jest-circus/src/eventHandler.ts similarity index 92% rename from packages/jest-circus/src/eventHandler.js rename to packages/jest-circus/src/eventHandler.ts index 663249c689a8..a29b69828d7b 100644 --- a/packages/jest-circus/src/eventHandler.js +++ b/packages/jest-circus/src/eventHandler.ts @@ -3,11 +3,9 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow strict-local */ -import type {EventHandler, Exception} from 'types/Circus'; +import {EventHandler, TEST_TIMEOUT_SYMBOL} from './types'; import { addErrorToEachTestUnderDescribe, @@ -22,9 +20,6 @@ import { restoreGlobalErrorHandlers, } from './globalErrorHandlers'; -// To pass this value from Runtime object to state we need to use global[sym] -const TEST_TIMEOUT_SYMBOL = Symbol.for('TEST_TIMEOUT_SYMBOL'); - const eventHandler: EventHandler = (event, state): void => { switch (event.name) { case 'include_test_location_in_result': { @@ -112,14 +107,14 @@ const eventHandler: EventHandler = (event, state): void => { if (type === 'beforeAll') { invariant(describeBlock, 'always present for `*All` hooks'); - addErrorToEachTestUnderDescribe(describeBlock, error, asyncError); + addErrorToEachTestUnderDescribe(describeBlock!, error, asyncError); } else if (type === 'afterAll') { // Attaching `afterAll` errors to each test makes execution flow // too complicated, so we'll consider them to be global. state.unhandledErrors.push([error, asyncError]); } else { invariant(test, 'always present for `*Each` hooks'); - test.errors.push([error, asyncError]); + test!.errors.push([error, asyncError]); } break; } @@ -152,8 +147,7 @@ const eventHandler: EventHandler = (event, state): void => { break; } case 'test_retry': { - const errors: Array<[?Exception, Exception]> = []; - event.test.errors = errors; + event.test.errors = []; break; } case 'run_start': { @@ -185,8 +179,8 @@ const eventHandler: EventHandler = (event, state): void => { invariant(state.originalGlobalErrorHandlers); invariant(state.parentProcess); restoreGlobalErrorHandlers( - state.parentProcess, - state.originalGlobalErrorHandlers, + state.parentProcess!, + state.originalGlobalErrorHandlers!, ); break; } diff --git a/packages/jest-circus/src/formatNodeAssertErrors.js b/packages/jest-circus/src/formatNodeAssertErrors.ts similarity index 81% rename from packages/jest-circus/src/formatNodeAssertErrors.js rename to packages/jest-circus/src/formatNodeAssertErrors.ts index ef84986451b6..8310f8fac781 100644 --- a/packages/jest-circus/src/formatNodeAssertErrors.js +++ b/packages/jest-circus/src/formatNodeAssertErrors.ts @@ -3,38 +3,29 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow strict-local */ -// $FlowFixMe: Converted to TS. It's also not exported, but should be imported from `matcher-utils` -import type {DiffOptions} from 'jest-diff'; -import type {Event, State} from 'types/Circus'; - -// $FlowFixMe: Converted to TS +import {AssertionError} from 'assert'; import {diff, printExpected, printReceived} from 'jest-matcher-utils'; import chalk from 'chalk'; -// $FlowFixMe: Converted to TS import prettyFormat from 'pretty-format'; +import {Event, State, TestError} from './types'; + +// TODO replace with import {DiffOptions} from 'jest-matcher-utils'; +type DiffOptions = Parameters[2]; + +interface AssertionErrorWithStack extends AssertionError { + stack: string; +} -type AssertionError = {| - actual: ?string, - expected: ?string, - generatedMessage: boolean, - message: string, - name: string, - operator: ?string, - stack: string, -|}; - -const assertOperatorsMap = { +const assertOperatorsMap: {[key: string]: string} = { '!=': 'notEqual', '!==': 'notStrictEqual', '==': 'equal', '===': 'strictEqual', }; -const humanReadableOperators = { +const humanReadableOperators: {[key: string]: string} = { deepEqual: 'to deeply equal', deepStrictEqual: 'to deeply and strictly equal', equal: 'to be equal', @@ -48,7 +39,7 @@ const humanReadableOperators = { const formatNodeAssertErrors = (event: Event, state: State) => { switch (event.name) { case 'test_done': { - event.test.errors = event.test.errors.map(errors => { + event.test.errors = event.test.errors.map((errors: TestError) => { let error; if (Array.isArray(errors)) { const [originalError, asyncError] = errors; @@ -75,7 +66,7 @@ const formatNodeAssertErrors = (event: Event, state: State) => { } }; -const getOperatorName = (operator: ?string, stack: string) => { +const getOperatorName = (operator: string | undefined, stack: string) => { if (typeof operator === 'string') { return assertOperatorsMap[operator] || operator; } @@ -88,9 +79,8 @@ const getOperatorName = (operator: ?string, stack: string) => { return ''; }; -const operatorMessage = (operator: ?string) => { +const operatorMessage = (operator: string | undefined) => { const niceOperatorName = getOperatorName(operator, ''); - // $FlowFixMe: we default to the operator itself, so holes in the map doesn't matter const humanReadableOperator = humanReadableOperators[niceOperatorName]; return typeof operator === 'string' @@ -104,7 +94,10 @@ const assertThrowingMatcherHint = (operatorName: string) => chalk.red('function') + chalk.dim(')'); -const assertMatcherHint = (operator: ?string, operatorName: string) => { +const assertMatcherHint = ( + operator: string | undefined | null, + operatorName: string, +) => { let message = chalk.dim('assert') + chalk.dim('.' + operatorName + '(') + @@ -125,7 +118,10 @@ const assertMatcherHint = (operator: ?string, operatorName: string) => { return message; }; -function assertionErrorMessage(error: AssertionError, options: DiffOptions) { +function assertionErrorMessage( + error: AssertionErrorWithStack, + options: DiffOptions, +) { const {expected, actual, generatedMessage, message, operator, stack} = error; const diffString = diff(expected, actual, options); const hasCustomMessage = !generatedMessage; diff --git a/packages/jest-circus/src/globalErrorHandlers.js b/packages/jest-circus/src/globalErrorHandlers.ts similarity index 94% rename from packages/jest-circus/src/globalErrorHandlers.js rename to packages/jest-circus/src/globalErrorHandlers.ts index e9ca53b2780d..5acd9ffac9a9 100644 --- a/packages/jest-circus/src/globalErrorHandlers.js +++ b/packages/jest-circus/src/globalErrorHandlers.ts @@ -3,12 +3,12 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow strict-local */ import {dispatch} from './state'; -import type {GlobalErrorHandlers} from 'types/Circus'; +import {GlobalErrorHandlers} from './types'; + +type Process = NodeJS.Process; const uncaught = (error: Error) => { dispatch({error, name: 'error'}); diff --git a/packages/jest-circus/src/index.js b/packages/jest-circus/src/index.js deleted file mode 100644 index ca770f8f07ca..000000000000 --- a/packages/jest-circus/src/index.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * 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. - * - * @flow strict-local - */ - -import type { - BlockFn, - HookFn, - HookType, - TestFn, - BlockMode, - BlockName, - TestName, - TestMode, -} from 'types/Circus'; -import {bind as bindEach} from 'jest-each'; -// $FlowFixMe: Converted to TS -import {ErrorWithStack} from 'jest-util'; -import {dispatch} from './state'; - -type THook = (fn: HookFn, timeout?: number) => void; - -const describe = (blockName: BlockName, blockFn: BlockFn) => - _dispatchDescribe(blockFn, blockName, describe); -describe.only = (blockName: BlockName, blockFn: BlockFn) => - _dispatchDescribe(blockFn, blockName, describe.only, 'only'); -describe.skip = (blockName: BlockName, blockFn: BlockFn) => - _dispatchDescribe(blockFn, blockName, describe.skip, 'skip'); - -const _dispatchDescribe = ( - blockFn, - blockName, - describeFn, - mode?: BlockMode, -) => { - const asyncError = new ErrorWithStack(undefined, describeFn); - if (blockFn === undefined) { - asyncError.message = `Missing second argument. It must be a callback function.`; - throw asyncError; - } - if (typeof blockFn !== 'function') { - asyncError.message = `Invalid second argument, ${blockFn}. It must be a callback function.`; - throw asyncError; - } - dispatch({ - asyncError, - blockName, - mode, - name: 'start_describe_definition', - }); - blockFn(); - dispatch({blockName, mode, name: 'finish_describe_definition'}); -}; - -const _addHook = (fn: HookFn, hookType: HookType, hookFn, timeout: ?number) => { - const asyncError = new ErrorWithStack(undefined, hookFn); - - if (typeof fn !== 'function') { - asyncError.message = - 'Invalid first argument. It must be a callback function.'; - - throw asyncError; - } - - dispatch({asyncError, fn, hookType, name: 'add_hook', timeout}); -}; - -// Hooks have to pass themselves to the HOF in order for us to trim stack traces. -const beforeEach: THook = (fn, timeout) => - _addHook(fn, 'beforeEach', beforeEach, timeout); -const beforeAll: THook = (fn, timeout) => - _addHook(fn, 'beforeAll', beforeAll, timeout); -const afterEach: THook = (fn, timeout) => - _addHook(fn, 'afterEach', afterEach, timeout); -const afterAll: THook = (fn, timeout) => - _addHook(fn, 'afterAll', afterAll, timeout); - -const test = (testName: TestName, fn: TestFn, timeout?: number) => - _addTest(testName, undefined, fn, test, timeout); -const it = test; -test.skip = (testName: TestName, fn?: TestFn, timeout?: number) => - _addTest(testName, 'skip', fn, test.skip, timeout); -test.only = (testName: TestName, fn: TestFn, timeout?: number) => - _addTest(testName, 'only', fn, test.only, timeout); -test.todo = (testName: TestName, ...rest: Array) => { - if (rest.length > 0 || typeof testName !== 'string') { - throw new ErrorWithStack( - 'Todo must be called with only a description.', - test.todo, - ); - } - return _addTest(testName, 'todo', () => {}, test.todo); -}; - -const _addTest = ( - testName: TestName, - mode: TestMode, - fn?: TestFn, - testFn, - timeout: ?number, -) => { - const asyncError = new ErrorWithStack(undefined, testFn); - - if (typeof testName !== 'string') { - asyncError.message = `Invalid first argument, ${testName}. It must be a string.`; - - throw asyncError; - } - if (fn === undefined) { - asyncError.message = - 'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.'; - - throw asyncError; - } - if (typeof fn !== 'function') { - asyncError.message = `Invalid second argument, ${fn}. It must be a callback function.`; - - throw asyncError; - } - - return dispatch({ - asyncError, - fn, - mode, - name: 'add_test', - testName, - timeout, - }); -}; - -test.each = bindEach(test); -test.only.each = bindEach(test.only); -test.skip.each = bindEach(test.skip); - -describe.each = bindEach(describe, false); -describe.only.each = bindEach(describe.only, false); -describe.skip.each = bindEach(describe.skip, false); - -module.exports = { - afterAll, - afterEach, - beforeAll, - beforeEach, - describe, - it, - test, -}; diff --git a/packages/jest-circus/src/index.ts b/packages/jest-circus/src/index.ts new file mode 100644 index 000000000000..d4ed333084b7 --- /dev/null +++ b/packages/jest-circus/src/index.ts @@ -0,0 +1,172 @@ +/** + * 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. + */ +// @ts-ignore TODO Remove ignore when jest-each is migrated to ts +import {bind as bindEach} from 'jest-each'; +import {ErrorWithStack} from 'jest-util'; +import {Global} from '@jest/types'; +import { + BlockFn, + HookFn, + HookType, + TestFn, + BlockMode, + BlockName, + TestName, + TestMode, +} from './types'; +import {dispatch} from './state'; + +type THook = (fn: HookFn, timeout?: number) => void; +type DescribeFn = (blockName: BlockName, blockFn: BlockFn) => void; + +const describe = (() => { + const describe = (blockName: BlockName, blockFn: BlockFn) => + _dispatchDescribe(blockFn, blockName, describe); + const only = (blockName: BlockName, blockFn: BlockFn) => + _dispatchDescribe(blockFn, blockName, only, 'only'); + const skip = (blockName: BlockName, blockFn: BlockFn) => + _dispatchDescribe(blockFn, blockName, skip, 'skip'); + + describe.each = bindEach(describe, false); + + only.each = bindEach(only, false); + skip.each = bindEach(skip, false); + + describe.only = only; + describe.skip = skip; + + return describe; +})(); + +const _dispatchDescribe = ( + blockFn: BlockFn, + blockName: BlockName, + describeFn: DescribeFn, + mode?: BlockMode, +) => { + const asyncError = new ErrorWithStack(undefined, describeFn); + if (blockFn === undefined) { + asyncError.message = `Missing second argument. It must be a callback function.`; + throw asyncError; + } + if (typeof blockFn !== 'function') { + asyncError.message = `Invalid second argument, ${blockFn}. It must be a callback function.`; + throw asyncError; + } + dispatch({ + asyncError, + blockName, + mode, + name: 'start_describe_definition', + }); + blockFn(); + dispatch({blockName, mode, name: 'finish_describe_definition'}); +}; + +const _addHook = ( + fn: HookFn, + hookType: HookType, + hookFn: THook, + timeout?: number, +) => { + const asyncError = new ErrorWithStack(undefined, hookFn); + + if (typeof fn !== 'function') { + asyncError.message = + 'Invalid first argument. It must be a callback function.'; + + throw asyncError; + } + + dispatch({asyncError, fn, hookType, name: 'add_hook', timeout}); +}; + +// Hooks have to pass themselves to the HOF in order for us to trim stack traces. +const beforeEach: THook = (fn, timeout) => + _addHook(fn, 'beforeEach', beforeEach, timeout); +const beforeAll: THook = (fn, timeout) => + _addHook(fn, 'beforeAll', beforeAll, timeout); +const afterEach: THook = (fn, timeout) => + _addHook(fn, 'afterEach', afterEach, timeout); +const afterAll: THook = (fn, timeout) => + _addHook(fn, 'afterAll', afterAll, timeout); + +const test: Global.It = (() => { + const test = (testName: TestName, fn: TestFn, timeout?: number): void => + _addTest(testName, undefined, fn, test, timeout); + const skip = (testName: TestName, fn?: TestFn, timeout?: number): void => + _addTest(testName, 'skip', fn, skip, timeout); + const only = (testName: TestName, fn: TestFn, timeout?: number): void => + _addTest(testName, 'only', fn, test.only, timeout); + + test.todo = (testName: TestName, ...rest: Array): void => { + if (rest.length > 0 || typeof testName !== 'string') { + throw new ErrorWithStack( + 'Todo must be called with only a description.', + test.todo, + ); + } + return _addTest(testName, 'todo', () => {}, test.todo); + }; + + const _addTest = ( + testName: TestName, + mode: TestMode, + fn: TestFn | undefined, + testFn: (testName: TestName, fn: TestFn, timeout?: number) => void, + timeout?: number, + ) => { + const asyncError = new ErrorWithStack(undefined, testFn); + + if (typeof testName !== 'string') { + asyncError.message = `Invalid first argument, ${testName}. It must be a string.`; + + throw asyncError; + } + if (fn === undefined) { + asyncError.message = + 'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.'; + + throw asyncError; + } + if (typeof fn !== 'function') { + asyncError.message = `Invalid second argument, ${fn}. It must be a callback function.`; + + throw asyncError; + } + + return dispatch({ + asyncError, + fn, + mode, + name: 'add_test', + testName, + timeout, + }); + }; + + test.each = bindEach(test); + only.each = bindEach(only); + skip.each = bindEach(skip); + + test.only = only; + test.skip = skip; + + return test; +})(); + +const it: Global.It = test; + +export = { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + it, + test, +}; diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.js b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts similarity index 81% rename from packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.js rename to packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index 8cabcb8a9f76..84f32c39e2f4 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.js +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -3,25 +3,23 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow strict-local */ -import type {Environment} from 'types/Environment'; -import type {GlobalConfig, ProjectConfig} from 'types/Config'; -import type {TestResult} from 'types/TestResult'; -import type Runtime from 'jest-runtime'; +import path from 'path'; +import {Config, TestResult, Environment} from '@jest/types'; +// @ts-ignore TODO Remove ignore when jest-runtime is migrated to ts +import Runtime from 'jest-runtime'; // eslint-disable-line import/no-extraneous-dependencies +import {SnapshotState} from 'jest-snapshot'; const FRAMEWORK_INITIALIZER = require.resolve('./jestAdapterInit'); -import path from 'path'; const jestAdapter = async ( - globalConfig: GlobalConfig, - config: ProjectConfig, - environment: Environment, + globalConfig: Config.GlobalConfig, + config: Config.ProjectConfig, + environment: Environment.$JestEnvironment, runtime: Runtime, testPath: string, -): Promise => { +): Promise => { const { initialize, runAndTransformResultsToJestFormat, @@ -84,7 +82,11 @@ const jestAdapter = async ( return _addSnapshotData(results, snapshotState); }; -const _addSnapshotData = (results: TestResult, snapshotState) => { +const _addSnapshotData = ( + results: TestResult.TestResult, + // TODO: make just snapshotState: SnapshotState when `jest-snapshot` is ESM + snapshotState: typeof SnapshotState.prototype, +) => { results.testResults.forEach(({fullName, status}) => { if (status === 'pending' || status === 'failed') { // if test is skipped or failed, we don't want to mark @@ -111,4 +113,4 @@ const _addSnapshotData = (results: TestResult, snapshotState) => { return results; }; -module.exports = jestAdapter; +export = jestAdapter; diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.js b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts similarity index 61% rename from packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.js rename to packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts index 16473b066aab..577a5e94431c 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.js +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts @@ -3,13 +3,9 @@ * * 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 {AssertionResult, TestResult, Status} from 'types/TestResult'; -import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; -import type {Event, RunResult, TestEntry} from 'types/Circus'; +import {Config, TestResult} from '@jest/types'; import {extractExpectedAssertionsErrors, getState, setState} from 'expect'; import {formatExecError, formatResultsErrors} from 'jest-message-util'; @@ -22,8 +18,10 @@ import throat from 'throat'; import {addEventHandler, dispatch, ROOT_DESCRIBE_BLOCK_NAME} from '../state'; import {getTestID} from '../utils'; import run from '../run'; -// eslint-disable-next-line import/default -import globals from '../index'; +import globals from '..'; +import {Event, RunResult, TestEntry} from '../types'; + +type Process = NodeJS.Process; export const initialize = ({ config, @@ -34,13 +32,13 @@ export const initialize = ({ parentProcess, testPath, }: { - config: ProjectConfig, - getPrettier: () => null | any, - getBabelTraverse: () => Function, - globalConfig: GlobalConfig, - localRequire: Path => any, - testPath: Path, - parentProcess: Process, + config: Config.ProjectConfig; + getPrettier: () => null | any; + getBabelTraverse: () => Function; + globalConfig: Config.GlobalConfig; + localRequire: (path: Config.Path) => any; + testPath: Config.Path; + parentProcess: Process; }) => { const mutex = throat(globalConfig.maxConcurrency); @@ -52,31 +50,36 @@ export const initialize = ({ global.fit = global.it.only; global.fdescribe = global.describe.only; - global.test.concurrent = ( - testName: string, - testFn: () => Promise, - timeout?: number, - ) => { - // For concurrent tests we first run the function that returns promise, and then register a - // nomral test that will be waiting on the returned promise (when we start the test, the promise - // will already be in the process of execution). - // Unfortunately at this stage there's no way to know if there are any `.only` tests in the suite - // that will result in this test to be skipped, so we'll be executing the promise function anyway, - // even if it ends up being skipped. - const promise = mutex(() => testFn()); - global.test(testName, () => promise, timeout); - }; + global.test.concurrent = (test => { + const concurrent = ( + testName: string, + testFn: () => Promise, + timeout?: number, + ) => { + // For concurrent tests we first run the function that returns promise, and then register a + // nomral test that will be waiting on the returned promise (when we start the test, the promise + // will already be in the process of execution). + // Unfortunately at this stage there's no way to know if there are any `.only` tests in the suite + // that will result in this test to be skipped, so we'll be executing the promise function anyway, + // even if it ends up being skipped. + const promise = mutex(() => testFn()); + global.test(testName, () => promise, timeout); + }; - global.test.concurrent.only = ( - testName: string, - testFn: () => Promise, - timeout?: number, - ) => { - const promise = mutex(() => testFn()); - global.test.only(testName, () => promise, timeout); - }; + concurrent.only = ( + testName: string, + testFn: () => Promise, + timeout?: number, + ) => { + const promise = mutex(() => testFn()); + // eslint-disable-next-line jest/no-focused-tests + test.only(testName, () => promise, timeout); + }; + + concurrent.skip = test.skip; - global.test.concurrent.skip = global.test.skip; + return concurrent; + })(global.test); addEventHandler(eventHandler); @@ -121,10 +124,10 @@ export const runAndTransformResultsToJestFormat = async ({ globalConfig, testPath, }: { - config: ProjectConfig, - globalConfig: GlobalConfig, - testPath: string, -}): Promise => { + config: Config.ProjectConfig; + globalConfig: Config.GlobalConfig; + testPath: string; +}): Promise => { const runResult: RunResult = await run(); let numFailingTests = 0; @@ -132,41 +135,43 @@ export const runAndTransformResultsToJestFormat = async ({ let numPendingTests = 0; let numTodoTests = 0; - 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], - }; - }, - ); + const assertionResults: Array< + TestResult.AssertionResult + > = runResult.testResults.map(testResult => { + let status: TestResult.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: title + ? ancestorTitles.concat(title).join(' ') + : ancestorTitles.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/legacy-code-todo-rewrite/jestExpect.js b/packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.ts similarity index 73% rename from packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.js rename to packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.ts index 51b24afa4082..a78f4da4f4ae 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.js +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.ts @@ -3,12 +3,8 @@ * * 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 {RawMatcherFn} from 'types/Matchers'; - import expect from 'expect'; import { @@ -19,12 +15,6 @@ import { toThrowErrorMatchingInlineSnapshot, } from 'jest-snapshot'; -type JasmineMatcher = { - (): JasmineMatcher, - compare: () => RawMatcherFn, - negativeCompare: () => RawMatcherFn, -}; - export default (config: {expand: boolean}) => { global.expect = expect; expect.setState({ @@ -37,5 +27,5 @@ export default (config: {expand: boolean}) => { toThrowErrorMatchingSnapshot, }); - (expect: Object).addSnapshotSerializer = addSerializer; + expect.addSnapshotSerializer = addSerializer; }; diff --git a/packages/jest-circus/src/run.js b/packages/jest-circus/src/run.ts similarity index 92% rename from packages/jest-circus/src/run.js rename to packages/jest-circus/src/run.ts index 27cadf1420bf..04d5b8459b8d 100644 --- a/packages/jest-circus/src/run.js +++ b/packages/jest-circus/src/run.ts @@ -3,17 +3,16 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow strict-local */ -import type { +import { RunResult, TestEntry, TestContext, Hook, DescribeBlock, -} from 'types/Circus'; + RETRY_TIMES, +} from './types'; import {getState, dispatch} from './state'; import { @@ -45,7 +44,7 @@ const _runTestsForDescribeBlock = async (describeBlock: DescribeBlock) => { } // Tests that fail and are retried we run after other tests - const retryTimes = parseInt(global[Symbol.for('RETRY_TIMES')], 10) || 0; + const retryTimes = parseInt(global[RETRY_TIMES], 10) || 0; const deferredRetryTests = []; for (const test of describeBlock.tests) { @@ -128,11 +127,11 @@ const _callCircusHook = ({ describeBlock, testContext, }: { - hook: Hook, - describeBlock?: DescribeBlock, - test?: TestEntry, - testContext?: TestContext, -}): Promise => { + hook: Hook; + describeBlock?: DescribeBlock; + test?: TestEntry; + testContext?: TestContext; +}): Promise => { dispatch({hook, name: 'hook_start'}); const timeout = hook.timeout || getState().testTimeout; return callAsyncCircusFn(hook.fn, testContext, {isHook: true, timeout}) @@ -155,7 +154,7 @@ const _callCircusTest = ( return Promise.resolve(); } - return callAsyncCircusFn(test.fn, testContext, {isHook: false, timeout}) + return callAsyncCircusFn(test.fn!, testContext, {isHook: false, timeout}) .then(() => dispatch({name: 'test_fn_success', test})) .catch(error => dispatch({error, name: 'test_fn_failure', test})); }; diff --git a/packages/jest-circus/src/state.js b/packages/jest-circus/src/state.ts similarity index 91% rename from packages/jest-circus/src/state.js rename to packages/jest-circus/src/state.ts index 8e353e7eb5c6..8f684d254d16 100644 --- a/packages/jest-circus/src/state.js +++ b/packages/jest-circus/src/state.ts @@ -3,11 +3,9 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow strict-local */ -import type {Event, State, EventHandler} from 'types/Circus'; +import {Event, State, EventHandler, STATE_SYM} from './types'; import {makeDescribe} from './utils'; import eventHandler from './eventHandler'; @@ -19,7 +17,6 @@ const eventHandlers: Array = [ ]; export const ROOT_DESCRIBE_BLOCK_NAME = 'ROOT_DESCRIBE_BLOCK'; -const STATE_SYM = Symbol('JEST_STATE_SYMBOL'); const ROOT_DESCRIBE_BLOCK = makeDescribe(ROOT_DESCRIBE_BLOCK_NAME); const INITIAL_STATE: State = { diff --git a/packages/jest-circus/src/types.ts b/packages/jest-circus/src/types.ts new file mode 100644 index 000000000000..bdc9cc8f2d1c --- /dev/null +++ b/packages/jest-circus/src/types.ts @@ -0,0 +1,243 @@ +/** + * 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. + */ + +// Used as type +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import expect from 'expect'; +import {Global} from '@jest/types'; + +type Process = NodeJS.Process; + +export type DoneFn = Global.DoneFn; +export type BlockFn = Global.BlockFn; +export type BlockName = Global.BlockName; +export type BlockMode = void | 'skip' | 'only' | 'todo'; +export type TestMode = BlockMode; +export type TestName = Global.TestName; +export type TestFn = Global.TestFn; +export type HookFn = (done?: DoneFn) => Promise | null | undefined; +export type AsyncFn = TestFn | HookFn; +export type SharedHookType = 'afterAll' | 'beforeAll'; +export type HookType = SharedHookType | 'afterEach' | 'beforeEach'; +export type TestContext = Object; +export type Exception = any; // Since in JS anything can be thrown as an error. +export type FormattedError = string; // String representation of error. +export type Hook = { + asyncError: Exception; + fn: HookFn; + type: HookType; + parent: DescribeBlock; + timeout: number | undefined | null; +}; + +export type EventHandler = (event: Event, state: State) => void; + +export type Event = + | { + name: 'include_test_location_in_result'; + } + | { + asyncError: Exception; + mode: BlockMode; + name: 'start_describe_definition'; + blockName: BlockName; + } + | { + mode: BlockMode; + name: 'finish_describe_definition'; + blockName: BlockName; + } + | { + asyncError: Exception; + name: 'add_hook'; + hookType: HookType; + fn: HookFn; + timeout: number | undefined; + } + | { + asyncError: Exception; + name: 'add_test'; + testName: TestName; + fn?: TestFn; + mode?: TestMode; + timeout: number | undefined; + } + | { + name: 'hook_start'; + hook: Hook; + } + | { + name: 'hook_success'; + describeBlock: DescribeBlock | undefined | null; + test: TestEntry | undefined | null; + hook: Hook; + } + | { + name: 'hook_failure'; + error: string | Exception; + describeBlock: DescribeBlock | undefined | null; + test: TestEntry | undefined | null; + hook: Hook; + } + | { + name: 'test_fn_start'; + test: TestEntry; + } + | { + name: 'test_fn_success'; + test: TestEntry; + } + | { + name: 'test_fn_failure'; + error: Exception; + test: TestEntry; + } + | { + name: 'test_retry'; + test: TestEntry; + } + | { + // the `test` in this case is all hooks + it/test function, not just the + // function passed to `it/test` + name: 'test_start'; + test: TestEntry; + } + | { + name: 'test_skip'; + test: TestEntry; + } + | { + name: 'test_todo'; + test: TestEntry; + } + | { + // test failure is defined by presence of errors in `test.errors`, + // `test_done` indicates that the test and all its hooks were run, + // and nothing else will change it's state in the future. (except third + // party extentions/plugins) + name: 'test_done'; + test: TestEntry; + } + | { + name: 'run_describe_start'; + describeBlock: DescribeBlock; + } + | { + name: 'run_describe_finish'; + describeBlock: DescribeBlock; + } + | { + name: 'run_start'; + } + | { + name: 'run_finish'; + } + | { + // Any unhandled error that happened outside of test/hooks (unless it is + // an `afterAll` hook) + name: 'error'; + error: Exception; + } + | { + // first action to dispatch. Good time to initialize all settings + name: 'setup'; + testNamePattern?: string; + parentProcess: Process; + } + | { + // Action dispatched after everything is finished and we're about to wrap + // things up and return test results to the parent process (caller). + name: 'teardown'; + }; + +export type TestStatus = 'skip' | 'done' | 'todo'; +export type TestResult = { + duration: number | null | undefined; + errors: Array; + invocations: number; + status: TestStatus; + location: {column: number; line: number} | null | undefined; + testPath: Array; +}; + +export type RunResult = { + unhandledErrors: Array; + testResults: TestResults; +}; + +export type TestResults = Array; + +export type GlobalErrorHandlers = { + uncaughtException: Array<(exception: Exception) => void>; + unhandledRejection: Array< + (exception: Exception, promise: Promise) => void + >; +}; + +export type State = { + currentDescribeBlock: DescribeBlock; + currentlyRunningTest: TestEntry | undefined | null; // including when hooks are being executed + expand?: boolean; // expand error messages + hasFocusedTests: boolean; // that are defined using test.only + // Store process error handlers. During the run we inject our own + // handlers (so we could fail tests on unhandled errors) and later restore + // the original ones. + originalGlobalErrorHandlers?: GlobalErrorHandlers; + parentProcess: Process | null; // process object from the outer scope + rootDescribeBlock: DescribeBlock; + testNamePattern: RegExp | undefined | null; + testTimeout: number; + unhandledErrors: Array; + includeTestLocationInResult: boolean; +}; + +export type DescribeBlock = { + children: Array; + hooks: Array; + mode: BlockMode; + name: BlockName; + parent: DescribeBlock | undefined | null; + tests: Array; +}; + +export type TestError = Exception | Array<[Exception | undefined, Exception]>; // the error from the test, as well as a backup error for async + +export type TestEntry = { + asyncError: Exception; // Used if the test failure contains no usable stack trace + errors: TestError; + fn: TestFn | undefined | null; + invocations: number; + mode: TestMode; + name: TestName; + parent: DescribeBlock; + startedAt: number | undefined | null; + duration: number | undefined | null; + status: TestStatus | undefined | null; // whether the test has been skipped or run already + timeout: number | undefined | null; +}; + +export const STATE_SYM = (Symbol( + 'JEST_STATE_SYMBOL', +) as unknown) as 'STATE_SYM_SYMBOL'; +export const RETRY_TIMES = (Symbol.for( + 'RETRY_TIMES', +) as unknown) as 'RETRY_TIMES_SYMBOL'; +// To pass this value from Runtime object to state we need to use global[sym] +export const TEST_TIMEOUT_SYMBOL = (Symbol.for( + 'TEST_TIMEOUT_SYMBOL', +) as unknown) as 'TEST_TIMEOUT_SYMBOL'; + +declare global { + module NodeJS { + interface Global { + STATE_SYM_SYMBOL: State; + RETRY_TIMES_SYMBOL: string; + TEST_TIMEOUT_SYMBOL: number; + expect: typeof expect; + } + } +} diff --git a/packages/jest-circus/src/utils.js b/packages/jest-circus/src/utils.ts similarity index 77% rename from packages/jest-circus/src/utils.js rename to packages/jest-circus/src/utils.ts index 0c56d8619b5b..7917d1356e9d 100644 --- a/packages/jest-circus/src/utils.js +++ b/packages/jest-circus/src/utils.ts @@ -3,11 +3,18 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow strict-local */ -import type { +import {convertDescriptorToString} from 'jest-util'; +import isGeneratorFn from 'is-generator-fn'; +import co from 'co'; + +import StackUtils from 'stack-utils'; + +import prettyFormat from 'pretty-format'; + +import {getState} from './state'; +import { AsyncFn, BlockMode, BlockName, @@ -21,24 +28,13 @@ import type { TestMode, TestName, TestResults, -} from 'types/Circus'; -// $FlowFixMe: Converted to TS -import {convertDescriptorToString} from 'jest-util'; -import isGeneratorFn from 'is-generator-fn'; -import co from 'co'; - -import StackUtils from 'stack-utils'; - -// $FlowFixMe: Converted to TS -import prettyFormat from 'pretty-format'; - -import {getState} from './state'; +} from './types'; const stackUtils = new StackUtils({cwd: 'A path that does not exist'}); export const makeDescribe = ( name: BlockName, - parent: ?DescribeBlock, + parent?: DescribeBlock, mode?: BlockMode, ): DescribeBlock => { let _mode = mode; @@ -58,29 +54,25 @@ export const makeDescribe = ( }; export const makeTest = ( - fn: ?TestFn, + fn: TestFn | undefined, mode: TestMode, name: TestName, parent: DescribeBlock, - timeout: ?number, + timeout: number | undefined, asyncError: Exception, -): TestEntry => { - const errors: Array<[?Exception, Exception]> = []; - - return { - asyncError, - duration: null, - errors, - fn, - invocations: 0, - mode, - name: convertDescriptorToString(name), - parent, - startedAt: null, - status: null, - timeout, - }; -}; +): TestEntry => ({ + asyncError, + duration: null, + errors: [], + fn, + invocations: 0, + mode, + name: convertDescriptorToString(name), + parent, + startedAt: null, + status: null, + timeout, +}); // Traverse the tree of describe blocks and return true if at least one describe // block has an enabled test. @@ -98,10 +90,14 @@ const hasEnabledTest = (describeBlock: DescribeBlock): boolean => { return hasOwnEnabledTests || describeBlock.children.some(hasEnabledTest); }; -export const getAllHooksForDescribe = ( - describe: DescribeBlock, -): {[key: 'beforeAll' | 'afterAll']: Array} => { - const result = {afterAll: [], beforeAll: []}; +export const getAllHooksForDescribe = (describe: DescribeBlock) => { + const result: { + beforeAll: Array; + afterAll: Array; + } = { + afterAll: [], + beforeAll: [], + }; if (hasEnabledTest(describe)) { for (const hook of describe.hooks) { @@ -119,11 +115,12 @@ export const getAllHooksForDescribe = ( return result; }; -export const getEachHooksForTest = ( - test: TestEntry, -): {[key: 'beforeEach' | 'afterEach']: Array} => { - const result = {afterEach: [], beforeEach: []}; - let {parent: block} = test; +export const getEachHooksForTest = (test: TestEntry) => { + const result: { + beforeEach: Array; + afterEach: Array; + } = {afterEach: [], beforeEach: []}; + let block: DescribeBlock | undefined | null = test.parent; do { const beforeEachForCurrentBlock = []; @@ -144,10 +141,10 @@ export const getEachHooksForTest = ( return result; }; -export const describeBlockHasTests = (describe: DescribeBlock) => - describe.tests.length || describe.children.some(describeBlockHasTests); +export const describeBlockHasTests = (describe: DescribeBlock): boolean => + describe.tests.length > 0 || describe.children.some(describeBlockHasTests); -const _makeTimeoutMessage = (timeout, isHook) => +const _makeTimeoutMessage = (timeout: number, isHook: boolean) => `Exceeded timeout of ${timeout}ms for a ${ isHook ? 'hook' : 'test' }.\nUse jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test.`; @@ -158,14 +155,14 @@ const {setTimeout, clearTimeout} = global; export const callAsyncCircusFn = ( fn: AsyncFn, - testContext: ?TestContext, - {isHook, timeout}: {isHook?: ?boolean, timeout: number}, -): Promise => { - let timeoutID; + 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)), + () => reject(_makeTimeoutMessage(timeout, !!isHook)), timeout, ); @@ -173,8 +170,8 @@ export const callAsyncCircusFn = ( // soon as `done` called. if (fn.length) { const done = (reason?: Error | string): void => { - // $FlowFixMe: It doesn't approve of .stack - const isError = reason && reason.message && reason.stack; + const isError = + reason && (reason as Error).message && (reason as Error).stack; return reason ? reject( isError @@ -236,7 +233,7 @@ export const callAsyncCircusFn = ( }); }; -export const getTestDuration = (test: TestEntry): ?number => { +export const getTestDuration = (test: TestEntry): number | null => { const {startedAt} = test; return typeof startedAt === 'number' ? Date.now() - startedAt : null; }; @@ -249,12 +246,12 @@ export const makeRunResult = ( unhandledErrors: unhandledErrors.map(_formatError), }); -const makeTestResults = (describeBlock: DescribeBlock, config): TestResults => { +const makeTestResults = (describeBlock: DescribeBlock): TestResults => { const {includeTestLocationInResult} = getState(); - let testResults = []; + let testResults: TestResults = []; for (const test of describeBlock.tests) { const testPath = []; - let parent = test; + let parent: TestEntry | DescribeBlock = test; do { testPath.unshift(parent.name); } while ((parent = parent.parent)); @@ -268,8 +265,17 @@ const makeTestResults = (describeBlock: DescribeBlock, config): TestResults => { let location = null; if (includeTestLocationInResult) { const stackLine = test.asyncError.stack.split('\n')[1]; - const {line, column} = stackUtils.parseLine(stackLine); - location = {column, line}; + const parsedLine = stackUtils.parseLine(stackLine); + if ( + parsedLine && + typeof parsedLine.column === 'number' && + typeof parsedLine.line === 'number' + ) { + location = { + column: parsedLine.column, + line: parsedLine.line, + }; + } } testResults.push({ @@ -283,7 +289,7 @@ const makeTestResults = (describeBlock: DescribeBlock, config): TestResults => { } for (const child of describeBlock.children) { - testResults = testResults.concat(makeTestResults(child, config)); + testResults = testResults.concat(makeTestResults(child)); } return testResults; @@ -293,7 +299,7 @@ const makeTestResults = (describeBlock: DescribeBlock, config): TestResults => { // names + test title) export const getTestID = (test: TestEntry) => { const titles = []; - let parent = test; + let parent: TestEntry | DescribeBlock = test; do { titles.unshift(parent.name); } while ((parent = parent.parent)); @@ -302,7 +308,9 @@ export const getTestID = (test: TestEntry) => { return titles.join(' '); }; -const _formatError = (errors: ?Exception | [?Exception, Exception]): string => { +const _formatError = ( + errors?: Exception | [Exception | undefined, Exception], +): string => { let error; let asyncError; @@ -342,7 +350,7 @@ export const addErrorToEachTestUnderDescribe = ( } }; -export const invariant = (condition: *, message: string) => { +export const invariant = (condition: unknown, message?: string) => { if (!condition) { throw new Error(message); } diff --git a/packages/jest-circus/tsconfig.json b/packages/jest-circus/tsconfig.json new file mode 100644 index 000000000000..3cb4125e6ac7 --- /dev/null +++ b/packages/jest-circus/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "build", + "rootDir": "src" + }, + "references": [{"path": "../jest-types"}, {"path": "../jest-snapshot"}, {"path": "../jest-matcher-utils"}, {"path": "../jest-message-util"}, {"path": "../jest-util"}, {"path": "../pretty-format"}, {"path": "../jest-diff"}] +} diff --git a/packages/jest-types/src/Environment.ts b/packages/jest-types/src/Environment.ts new file mode 100644 index 000000000000..1af4493db851 --- /dev/null +++ b/packages/jest-types/src/Environment.ts @@ -0,0 +1,44 @@ +/** + * 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 {Script} from 'vm'; +import {ProjectConfig} from './Config'; +import {Global} from './Global'; + +// TODO Fix jest-mock and @jest/types has circular dependency +// import {ModuleMocker} from 'jest-mock'; +type ModuleMocker = any; + +export type EnvironmentContext = { + console?: Object; + testPath?: string; +}; + +export declare class $JestEnvironment { + constructor(config: ProjectConfig, context?: EnvironmentContext); + runScript(script: Script): any; + global: Global; + fakeTimers: { + clearAllTimers(): void; + runAllImmediates(): void; + runAllTicks(): void; + runAllTimers(): void; + advanceTimersByTime(msToRun: number): void; + runOnlyPendingTimers(): void; + runWithRealTimers(callback: any): void; + getTimerCount(): number; + useFakeTimers(): void; + useRealTimers(): void; + }; + testFilePath: string; + moduleMocker: ModuleMocker; + setup(): Promise; + teardown(): Promise; +} + +export type Environment = $JestEnvironment; +export type EnvironmentClass = typeof $JestEnvironment; diff --git a/packages/jest-types/src/Global.ts b/packages/jest-types/src/Global.ts new file mode 100644 index 000000000000..ed7739615e14 --- /dev/null +++ b/packages/jest-types/src/Global.ts @@ -0,0 +1,75 @@ +/** + * 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. + */ + +export type DoneFn = (reason?: string | Error) => void; +export type TestName = string; +export type TestFn = (done?: DoneFn) => Promise | void | undefined; +export type BlockFn = () => void; +export type BlockName = string; + +// TODO Replace with actual type when `jest-each` is ready +type Each = () => void; + +export interface ItBase { + (testName: TestName, fn: TestFn, timeout?: number): void; + each: Each; +} + +export interface It extends ItBase { + only: ItBase; + skip: ItBase; + todo: (testName: TestName, ...rest: Array) => void; +} + +export interface ItConcurrentBase { + (testName: string, testFn: () => Promise, timeout?: number): void; +} + +export interface ItConcurrentExtended extends ItConcurrentBase { + only: ItConcurrentBase; + skip: ItConcurrentBase; +} + +export interface ItConcurrent extends It { + concurrent: ItConcurrentExtended; +} + +export interface DescribeBase { + (blockName: BlockName, blockFn: BlockFn): void; + each: Each; +} + +export interface Describe extends DescribeBase { + only: ItBase; + skip: ItBase; +} + +export interface Global { + it: It; + test: ItConcurrent; + fit: ItBase; + xit: ItBase; + xtest: ItBase; + describe: Describe; + xdescribe: DescribeBase; + fdescribe: DescribeBase; +} + +declare global { + module NodeJS { + interface Global { + it: It; + test: ItConcurrent; + fit: ItBase; + xit: ItBase; + xtest: ItBase; + describe: Describe; + xdescribe: DescribeBase; + fdescribe: DescribeBase; + } + } +} diff --git a/packages/jest-types/src/index.ts b/packages/jest-types/src/index.ts index 0226a0a00820..d845e25a2ff3 100644 --- a/packages/jest-types/src/index.ts +++ b/packages/jest-types/src/index.ts @@ -14,6 +14,8 @@ import * as Resolve from './Resolve'; import * as Snapshot from './Snapshot'; import * as SourceMaps from './SourceMaps'; import * as TestResult from './TestResult'; +import * as Global from './Global'; +import * as Environment from './Environment'; export { Config, @@ -25,4 +27,6 @@ export { Snapshot, SourceMaps, TestResult, + Global, + Environment, }; diff --git a/yarn.lock b/yarn.lock index cfedeb1ee38a..3a7d5cb3e17a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1546,6 +1546,11 @@ resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.10.tgz#780d552467824be4a241b29510a7873a7432c4a6" integrity sha512-fOM/Jhv51iyugY7KOBZz2ThfT1gwvsGCfWxpLpZDgkGjpEO4Le9cld07OdskikLjDUQJ43dzDaVRSFwQlpdqVg== +"@types/co@^4.6.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@types/co/-/co-4.6.0.tgz#fd7b669f3643e366d2d2114022be0571f3ddfc68" + integrity sha512-Ptqc3o9M/zuYT/AjM5pVO7xsAXPkal6P5xJ1nIbYYRzMlFYn7ZAVj0Wzz/Falort7jasH7GxuCkx5iueiiyjJA== + "@types/color-name@*": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.0.tgz#926f76f7e66f49cc59ad880bb15b030abbf0b66d"