From 31e06e8a747241b1a3a2d25dab01f4ff8faf5106 Mon Sep 17 00:00:00 2001 From: scotthovestadt Date: Fri, 19 Apr 2019 12:39:27 -0700 Subject: [PATCH] Bind to Circus events via an optional event handler on any custom env. (#8344) * Bind to Circus events via an optional event handler on any custom env. * Fix lint error. * Update CHANGELOG.md * Move Circus types into @jest/types * Move handleTestEvent definition into JestEnvironment * Fix linter errors. * Add test for Circus events being fired from environmet. --- CHANGELOG.md | 1 + docs/Configuration.md | 8 + e2e/__tests__/testEnvironmentCircus.test.ts | 46 ++++ .../CircusHandleTestEventEnvironment.js | 13 ++ .../__tests__/circusHandleTestEvent.test.js | 15 ++ e2e/test-environment-circus/package.json | 5 + packages/jest-circus/README.md | 19 ++ .../src/__mocks__/testEventHandler.ts | 4 +- packages/jest-circus/src/eventHandler.ts | 5 +- .../jest-circus/src/formatNodeAssertErrors.ts | 6 +- .../jest-circus/src/globalErrorHandlers.ts | 6 +- packages/jest-circus/src/index.ts | 77 ++++--- .../legacy-code-todo-rewrite/jestAdapter.ts | 1 + .../jestAdapterInit.ts | 18 +- packages/jest-circus/src/run.ts | 30 ++- packages/jest-circus/src/state.ts | 16 +- packages/jest-circus/src/types.ts | 215 +---------------- packages/jest-circus/src/utils.ts | 89 +++---- packages/jest-environment/src/index.ts | 3 +- packages/jest-types/src/Circus.ts | 218 ++++++++++++++++++ packages/jest-types/src/index.ts | 3 +- 21 files changed, 459 insertions(+), 339 deletions(-) create mode 100644 e2e/__tests__/testEnvironmentCircus.test.ts create mode 100644 e2e/test-environment-circus/CircusHandleTestEventEnvironment.js create mode 100644 e2e/test-environment-circus/__tests__/circusHandleTestEvent.test.js create mode 100644 e2e/test-environment-circus/package.json create mode 100644 packages/jest-types/src/Circus.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 874df7600df7..ad6df5adf66c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[jest-circus]` Bind to Circus events via an optional event handler on any custom env ([#8344](https://github.com/facebook/jest/pull/8344) - `[expect]` Improve report when matcher fails, part 15 ([#8281](https://github.com/facebook/jest/pull/8281)) - `[jest-cli]` Update `--forceExit` and "did not exit for one second" message colors ([#8329](https://github.com/facebook/jest/pull/8329)) - `[expect]` Improve report when matcher fails, part 16 ([#8306](https://github.com/facebook/jest/pull/8306)) diff --git a/docs/Configuration.md b/docs/Configuration.md index 01cd50f533e9..cd93bc214f34 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -861,6 +861,8 @@ test('use jsdom in this test file', () => { You can create your own module that will be used for setting up the test environment. The module must export a class with `setup`, `teardown` and `runScript` methods. You can also pass variables from this module to your test suites by assigning them to `this.global` object – this will make them available in your test suites as global variables. +The class may optionally expose a `handleTestEvent` method to bind to events fired by [`jest-circus`](https://github.com/facebook/jest/tree/master/packages/jest-circus). + Any docblock pragmas in test files will be passed to the environment constructor and can be used for per-test configuration. If the pragma does not have a value, it will be present in the object with it's value set to an empty string. If the pragma is not present, it will not be present in the object. _Note: TestEnvironment is sandboxed. Each test suite will trigger setup/teardown in their own TestEnvironment._ @@ -898,6 +900,12 @@ class CustomEnvironment extends NodeEnvironment { runScript(script) { return super.runScript(script); } + + handleTestEvent(event, state) { + if (event.name === 'test_start') { + // ... + } + } } module.exports = CustomEnvironment; diff --git a/e2e/__tests__/testEnvironmentCircus.test.ts b/e2e/__tests__/testEnvironmentCircus.test.ts new file mode 100644 index 000000000000..12ff9e833ab8 --- /dev/null +++ b/e2e/__tests__/testEnvironmentCircus.test.ts @@ -0,0 +1,46 @@ +/** + * 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 {skipSuiteOnJasmine} from '@jest/test-utils'; +import runJest from '../runJest'; + +skipSuiteOnJasmine(); + +it('calls testEnvironment handleTestEvent', () => { + const result = runJest('test-environment-circus'); + expect(result.failed).toEqual(false); + expect(result.stdout.split('\n')).toMatchInlineSnapshot(` + Array [ + "setup", + "add_hook", + "add_hook", + "add_test", + "add_test", + "run_start", + "run_describe_start", + "test_start: test name here", + "hook_start", + "hook_success: test name here", + "hook_start", + "hook_success: test name here", + "test_fn_start: test name here", + "test_fn_success: test name here", + "test_done: test name here", + "test_start: second test name here", + "hook_start", + "hook_success: second test name here", + "hook_start", + "hook_success: second test name here", + "test_fn_start: second test name here", + "test_fn_success: second test name here", + "test_done: second test name here", + "run_describe_finish", + "run_finish", + "teardown", + ] + `); +}); diff --git a/e2e/test-environment-circus/CircusHandleTestEventEnvironment.js b/e2e/test-environment-circus/CircusHandleTestEventEnvironment.js new file mode 100644 index 000000000000..f06dc57e368a --- /dev/null +++ b/e2e/test-environment-circus/CircusHandleTestEventEnvironment.js @@ -0,0 +1,13 @@ +// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + +'use strict'; + +const JSDOMEnvironment = require('jest-environment-jsdom'); + +class TestEnvironment extends JSDOMEnvironment { + handleTestEvent(event) { + console.log(event.name + (event.test ? ': ' + event.test.name : '')); + } +} + +module.exports = TestEnvironment; diff --git a/e2e/test-environment-circus/__tests__/circusHandleTestEvent.test.js b/e2e/test-environment-circus/__tests__/circusHandleTestEvent.test.js new file mode 100644 index 000000000000..6f90232e4c55 --- /dev/null +++ b/e2e/test-environment-circus/__tests__/circusHandleTestEvent.test.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * @jest-environment ./CircusHandleTestEventEnvironment.js + */ + +beforeEach(() => {}); + +test('test name here', () => { + expect(true).toBe(true); +}); + +test('second test name here', () => { + expect(true).toBe(true); +}); diff --git a/e2e/test-environment-circus/package.json b/e2e/test-environment-circus/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/test-environment-circus/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/packages/jest-circus/README.md b/packages/jest-circus/README.md index af963748aa87..0c5e282ffc3e 100644 --- a/packages/jest-circus/README.md +++ b/packages/jest-circus/README.md @@ -9,6 +9,25 @@ Circus is a flux-based test runner for Jest that is fast, easy to maintain, and simple to extend. +Circus allows you to bind to events via an optional event handler on any [custom environment](https://jestjs.io/docs/en/configuration#testenvironment-string). See the [type definitions](https://github.com/facebook/jest/blob/master/packages/jest-circus/src/types.ts) for more information on the events and state data currently available. + +```js +import {NodeEnvironment} from 'jest-environment-node'; +import {Event, State} from 'jest-circus'; + +class MyCustomEnvironment extends NodeEnvironment { + //... + + handleTestEvent(event: Event, state: State) { + if (event.name === 'test_start') { + // ... + } + } +} +``` + +Mutating event or state data is currently unsupported and may cause unexpected behavior or break in a future release without warning. New events, event data, and/or state data will not be considered a breaking change and may be added in any minor release. + ## Installation Install `jest-circus` using yarn: diff --git a/packages/jest-circus/src/__mocks__/testEventHandler.ts b/packages/jest-circus/src/__mocks__/testEventHandler.ts index 1bc877b075f9..6ae7ab7fa2be 100644 --- a/packages/jest-circus/src/__mocks__/testEventHandler.ts +++ b/packages/jest-circus/src/__mocks__/testEventHandler.ts @@ -6,9 +6,9 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -import {EventHandler} from '../types'; +import {Circus} from '@jest/types'; -const testEventHandler: EventHandler = (event, state) => { +const testEventHandler: Circus.EventHandler = (event, state) => { switch (event.name) { case 'start_describe_definition': case 'finish_describe_definition': { diff --git a/packages/jest-circus/src/eventHandler.ts b/packages/jest-circus/src/eventHandler.ts index a29b69828d7b..1accee9c36a0 100644 --- a/packages/jest-circus/src/eventHandler.ts +++ b/packages/jest-circus/src/eventHandler.ts @@ -5,7 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import {EventHandler, TEST_TIMEOUT_SYMBOL} from './types'; +import {Circus} from '@jest/types'; +import {TEST_TIMEOUT_SYMBOL} from './types'; import { addErrorToEachTestUnderDescribe, @@ -20,7 +21,7 @@ import { restoreGlobalErrorHandlers, } from './globalErrorHandlers'; -const eventHandler: EventHandler = (event, state): void => { +const eventHandler: Circus.EventHandler = (event, state): void => { switch (event.name) { case 'include_test_location_in_result': { state.includeTestLocationInResult = true; diff --git a/packages/jest-circus/src/formatNodeAssertErrors.ts b/packages/jest-circus/src/formatNodeAssertErrors.ts index 9fa8f0031dd2..3ad52ac72117 100644 --- a/packages/jest-circus/src/formatNodeAssertErrors.ts +++ b/packages/jest-circus/src/formatNodeAssertErrors.ts @@ -6,6 +6,7 @@ */ import {AssertionError} from 'assert'; +import {Circus} from '@jest/types'; import { diff, printExpected, @@ -14,7 +15,6 @@ import { } from 'jest-matcher-utils'; import chalk from 'chalk'; import prettyFormat from 'pretty-format'; -import {Event, State, TestError} from './types'; interface AssertionErrorWithStack extends AssertionError { stack: string; @@ -38,10 +38,10 @@ const humanReadableOperators: {[key: string]: string} = { strictEqual: 'to strictly be equal', }; -const formatNodeAssertErrors = (event: Event, state: State) => { +const formatNodeAssertErrors = (event: Circus.Event, state: Circus.State) => { switch (event.name) { case 'test_done': { - event.test.errors = event.test.errors.map((errors: TestError) => { + event.test.errors = event.test.errors.map((errors: Circus.TestError) => { let error; if (Array.isArray(errors)) { const [originalError, asyncError] = errors; diff --git a/packages/jest-circus/src/globalErrorHandlers.ts b/packages/jest-circus/src/globalErrorHandlers.ts index 0bf47f870d70..a48763f8f24c 100644 --- a/packages/jest-circus/src/globalErrorHandlers.ts +++ b/packages/jest-circus/src/globalErrorHandlers.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import {Circus} from '@jest/types'; import {dispatch} from './state'; -import {GlobalErrorHandlers} from './types'; const uncaught: NodeJS.UncaughtExceptionListener & NodeJS.UnhandledRejectionListener = (error: unknown) => { @@ -15,7 +15,7 @@ const uncaught: NodeJS.UncaughtExceptionListener & export const injectGlobalErrorHandlers = ( parentProcess: NodeJS.Process, -): GlobalErrorHandlers => { +): Circus.GlobalErrorHandlers => { const uncaughtException = process.listeners('uncaughtException').slice(); const unhandledRejection = process.listeners('unhandledRejection').slice(); parentProcess.removeAllListeners('uncaughtException'); @@ -27,7 +27,7 @@ export const injectGlobalErrorHandlers = ( export const restoreGlobalErrorHandlers = ( parentProcess: NodeJS.Process, - originalErrorHandlers: GlobalErrorHandlers, + originalErrorHandlers: Circus.GlobalErrorHandlers, ) => { parentProcess.removeListener('uncaughtException', uncaught); parentProcess.removeListener('unhandledRejection', uncaught); diff --git a/packages/jest-circus/src/index.ts b/packages/jest-circus/src/index.ts index b7e93f3da0f4..f7dc64781f81 100644 --- a/packages/jest-circus/src/index.ts +++ b/packages/jest-circus/src/index.ts @@ -9,28 +9,21 @@ import chalk from 'chalk'; import {bind as bindEach} from 'jest-each'; import {formatExecError} from 'jest-message-util'; import {ErrorWithStack, isPromise} from 'jest-util'; -import {Global} from '@jest/types'; -import { - BlockFn, - HookFn, - HookType, - TestFn, - BlockMode, - BlockName, - TestName, - TestMode, -} from './types'; +import {Circus, Global} from '@jest/types'; import {dispatch} from './state'; -type THook = (fn: HookFn, timeout?: number) => void; -type DescribeFn = (blockName: BlockName, blockFn: BlockFn) => void; +type THook = (fn: Circus.HookFn, timeout?: number) => void; +type DescribeFn = ( + blockName: Circus.BlockName, + blockFn: Circus.BlockFn, +) => void; const describe = (() => { - const describe = (blockName: BlockName, blockFn: BlockFn) => + const describe = (blockName: Circus.BlockName, blockFn: Circus.BlockFn) => _dispatchDescribe(blockFn, blockName, describe); - const only = (blockName: BlockName, blockFn: BlockFn) => + const only = (blockName: Circus.BlockName, blockFn: Circus.BlockFn) => _dispatchDescribe(blockFn, blockName, only, 'only'); - const skip = (blockName: BlockName, blockFn: BlockFn) => + const skip = (blockName: Circus.BlockName, blockFn: Circus.BlockFn) => _dispatchDescribe(blockFn, blockName, skip, 'skip'); describe.each = bindEach(describe, false); @@ -45,10 +38,10 @@ const describe = (() => { })(); const _dispatchDescribe = ( - blockFn: BlockFn, - blockName: BlockName, + blockFn: Circus.BlockFn, + blockName: Circus.BlockName, describeFn: DescribeFn, - mode?: BlockMode, + mode?: Circus.BlockMode, ) => { const asyncError = new ErrorWithStack(undefined, describeFn); if (blockFn === undefined) { @@ -102,8 +95,8 @@ const _dispatchDescribe = ( }; const _addHook = ( - fn: HookFn, - hookType: HookType, + fn: Circus.HookFn, + hookType: Circus.HookType, hookFn: THook, timeout?: number, ) => { @@ -130,14 +123,23 @@ 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 => { + const test = ( + testName: Circus.TestName, + fn: Circus.TestFn, + timeout?: number, + ): void => _addTest(testName, undefined, fn, test, timeout); + const skip = ( + testName: Circus.TestName, + fn?: Circus.TestFn, + timeout?: number, + ): void => _addTest(testName, 'skip', fn, skip, timeout); + const only = ( + testName: Circus.TestName, + fn: Circus.TestFn, + timeout?: number, + ): void => _addTest(testName, 'only', fn, test.only, timeout); + + test.todo = (testName: Circus.TestName, ...rest: Array): void => { if (rest.length > 0 || typeof testName !== 'string') { throw new ErrorWithStack( 'Todo must be called with only a description.', @@ -148,10 +150,14 @@ const test: Global.It = (() => { }; const _addTest = ( - testName: TestName, - mode: TestMode, - fn: TestFn | undefined, - testFn: (testName: TestName, fn: TestFn, timeout?: number) => void, + testName: Circus.TestName, + mode: Circus.TestMode, + fn: Circus.TestFn | undefined, + testFn: ( + testName: Circus.TestName, + fn: Circus.TestFn, + timeout?: number, + ) => void, timeout?: number, ) => { const asyncError = new ErrorWithStack(undefined, testFn); @@ -195,7 +201,10 @@ const test: Global.It = (() => { const it: Global.It = test; -export = { +export type Event = Circus.Event; +export type State = Circus.State; +export {afterAll, afterEach, beforeAll, beforeEach, describe, it, test}; +export default { afterAll, afterEach, beforeAll, diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index d255c9b4661c..6ee6a44c9b39 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -39,6 +39,7 @@ const jestAdapter = async ( const {globals, snapshotState} = initialize({ config, + environment, getBabelTraverse, getPrettier, globalConfig, diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts index 40f93dbf3c0b..434dd63675f1 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts @@ -5,7 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import {Config} from '@jest/types'; +import {Circus, Config} from '@jest/types'; +import {JestEnvironment} from '@jest/environment'; import {AssertionResult, Status, TestResult} from '@jest/test-result'; import {extractExpectedAssertionsErrors, getState, setState} from 'expect'; import {formatExecError, formatResultsErrors} from 'jest-message-util'; @@ -19,12 +20,12 @@ import {addEventHandler, dispatch, ROOT_DESCRIBE_BLOCK_NAME} from '../state'; import {getTestID} from '../utils'; import run from '../run'; import globals from '..'; -import {Event, RunResult, TestEntry} from '../types'; type Process = NodeJS.Process; export const initialize = ({ config, + environment, getPrettier, getBabelTraverse, globalConfig, @@ -33,6 +34,7 @@ export const initialize = ({ testPath, }: { config: Config.ProjectConfig; + environment: JestEnvironment; getPrettier: () => null | any; getBabelTraverse: () => Function; globalConfig: Config.GlobalConfig; @@ -83,6 +85,10 @@ export const initialize = ({ addEventHandler(eventHandler); + if (environment.handleTestEvent) { + addEventHandler(environment.handleTestEvent.bind(environment)); + } + dispatch({ name: 'setup', parentProcess, @@ -128,7 +134,7 @@ export const runAndTransformResultsToJestFormat = async ({ globalConfig: Config.GlobalConfig; testPath: string; }): Promise => { - const runResult: RunResult = await run(); + const runResult: Circus.RunResult = await run(); let numFailingTests = 0; let numPassingTests = 0; @@ -227,7 +233,7 @@ export const runAndTransformResultsToJestFormat = async ({ }; }; -const eventHandler = (event: Event) => { +const eventHandler = (event: Circus.Event) => { switch (event.name) { case 'test_start': { setState({currentTestName: getTestID(event.test)}); @@ -241,7 +247,7 @@ const eventHandler = (event: Event) => { } }; -const _addExpectedAssertionErrors = (test: TestEntry) => { +const _addExpectedAssertionErrors = (test: Circus.TestEntry) => { const failures = extractExpectedAssertionsErrors(); const errors = failures.map(failure => failure.error); test.errors = test.errors.concat(errors); @@ -250,7 +256,7 @@ const _addExpectedAssertionErrors = (test: TestEntry) => { // Get suppressed errors from ``jest-matchers`` that weren't throw during // test execution and add them to the test result, potentially failing // a passing test. -const _addSuppressedErrors = (test: TestEntry) => { +const _addSuppressedErrors = (test: Circus.TestEntry) => { const {suppressedErrors} = getState(); setState({suppressedErrors: []}); if (suppressedErrors.length) { diff --git a/packages/jest-circus/src/run.ts b/packages/jest-circus/src/run.ts index fcff37cd826a..f289a7501d24 100644 --- a/packages/jest-circus/src/run.ts +++ b/packages/jest-circus/src/run.ts @@ -5,14 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import { - RunResult, - TestEntry, - TestContext, - Hook, - DescribeBlock, - RETRY_TIMES, -} from './types'; +import {Circus} from '@jest/types'; +import {RETRY_TIMES} from './types'; import {getState, dispatch} from './state'; import { @@ -24,7 +18,7 @@ import { makeRunResult, } from './utils'; -const run = async (): Promise => { +const run = async (): Promise => { const {rootDescribeBlock} = getState(); dispatch({name: 'run_start'}); await _runTestsForDescribeBlock(rootDescribeBlock); @@ -35,7 +29,9 @@ const run = async (): Promise => { ); }; -const _runTestsForDescribeBlock = async (describeBlock: DescribeBlock) => { +const _runTestsForDescribeBlock = async ( + describeBlock: Circus.DescribeBlock, +) => { dispatch({describeBlock, name: 'run_describe_start'}); const {beforeAll, afterAll} = getAllHooksForDescribe(describeBlock); @@ -83,7 +79,7 @@ const _runTestsForDescribeBlock = async (describeBlock: DescribeBlock) => { dispatch({describeBlock, name: 'run_describe_finish'}); }; -const _runTest = async (test: TestEntry): Promise => { +const _runTest = async (test: Circus.TestEntry): Promise => { dispatch({name: 'test_start', test}); const testContext = Object.create(null); const {hasFocusedTests, testNamePattern} = getState(); @@ -132,10 +128,10 @@ const _callCircusHook = ({ describeBlock, testContext, }: { - hook: Hook; - describeBlock?: DescribeBlock; - test?: TestEntry; - testContext?: TestContext; + hook: Circus.Hook; + describeBlock?: Circus.DescribeBlock; + test?: Circus.TestEntry; + testContext?: Circus.TestContext; }): Promise => { dispatch({hook, name: 'hook_start'}); const timeout = hook.timeout || getState().testTimeout; @@ -147,8 +143,8 @@ const _callCircusHook = ({ }; const _callCircusTest = ( - test: TestEntry, - testContext: TestContext, + test: Circus.TestEntry, + testContext: Circus.TestContext, ): Promise => { dispatch({name: 'test_fn_start', test}); const timeout = test.timeout || getState().testTimeout; diff --git a/packages/jest-circus/src/state.ts b/packages/jest-circus/src/state.ts index 8f684d254d16..d42928e90bfa 100644 --- a/packages/jest-circus/src/state.ts +++ b/packages/jest-circus/src/state.ts @@ -5,13 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import {Event, State, EventHandler, STATE_SYM} from './types'; +import {Circus} from '@jest/types'; +import {STATE_SYM} from './types'; import {makeDescribe} from './utils'; import eventHandler from './eventHandler'; import formatNodeAssertErrors from './formatNodeAssertErrors'; -const eventHandlers: Array = [ +const eventHandlers: Array = [ eventHandler, formatNodeAssertErrors, ]; @@ -19,7 +20,7 @@ const eventHandlers: Array = [ export const ROOT_DESCRIBE_BLOCK_NAME = 'ROOT_DESCRIBE_BLOCK'; const ROOT_DESCRIBE_BLOCK = makeDescribe(ROOT_DESCRIBE_BLOCK_NAME); -const INITIAL_STATE: State = { +const INITIAL_STATE: Circus.State = { currentDescribeBlock: ROOT_DESCRIBE_BLOCK, currentlyRunningTest: null, expand: undefined, @@ -34,15 +35,16 @@ const INITIAL_STATE: State = { global[STATE_SYM] = INITIAL_STATE; -export const getState = (): State => global[STATE_SYM]; -export const setState = (state: State): State => (global[STATE_SYM] = state); +export const getState = (): Circus.State => global[STATE_SYM]; +export const setState = (state: Circus.State): Circus.State => + (global[STATE_SYM] = state); -export const dispatch = (event: Event): void => { +export const dispatch = (event: Circus.Event): void => { for (const handler of eventHandlers) { handler(event, getState()); } }; -export const addEventHandler = (handler: EventHandler): void => { +export const addEventHandler = (handler: Circus.EventHandler): void => { eventHandlers.push(handler); }; diff --git a/packages/jest-circus/src/types.ts b/packages/jest-circus/src/types.ts index 87bf25bce902..623dcd3c6ed9 100644 --- a/packages/jest-circus/src/types.ts +++ b/packages/jest-circus/src/types.ts @@ -8,217 +8,8 @@ // 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 = Record; -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: Error; - 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: Error; - mode: BlockMode; - name: 'start_describe_definition'; - blockName: BlockName; - } - | { - mode: BlockMode; - name: 'finish_describe_definition'; - blockName: BlockName; - } - | { - asyncError: Error; - name: 'add_hook'; - hookType: HookType; - fn: HookFn; - timeout: number | undefined; - } - | { - asyncError: Error; - 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; -}; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import {Circus} from '@jest/types'; export const STATE_SYM = (Symbol( 'JEST_STATE_SYMBOL', @@ -234,7 +25,7 @@ export const TEST_TIMEOUT_SYMBOL = (Symbol.for( declare global { module NodeJS { interface Global { - STATE_SYM_SYMBOL: State; + STATE_SYM_SYMBOL: Circus.State; RETRY_TIMES_SYMBOL: string; TEST_TIMEOUT_SYMBOL: number; expect: typeof expect; diff --git a/packages/jest-circus/src/utils.ts b/packages/jest-circus/src/utils.ts index fbad7a725c77..433f0c59ab77 100644 --- a/packages/jest-circus/src/utils.ts +++ b/packages/jest-circus/src/utils.ts @@ -5,38 +5,21 @@ * LICENSE file in the root directory of this source tree. */ +import {Circus} from '@jest/types'; 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, - DescribeBlock, - Exception, - Hook, - RunResult, - TestEntry, - TestContext, - TestFn, - TestMode, - TestName, - TestResults, -} from './types'; const stackUtils = new StackUtils({cwd: 'A path that does not exist'}); export const makeDescribe = ( - name: BlockName, - parent?: DescribeBlock, - mode?: BlockMode, -): DescribeBlock => { + name: Circus.BlockName, + parent?: Circus.DescribeBlock, + mode?: Circus.BlockMode, +): Circus.DescribeBlock => { let _mode = mode; if (parent && !mode) { // If not set explicitly, inherit from the parent describe. @@ -54,13 +37,13 @@ export const makeDescribe = ( }; export const makeTest = ( - fn: TestFn | undefined, - mode: TestMode, - name: TestName, - parent: DescribeBlock, + fn: Circus.TestFn | undefined, + mode: Circus.TestMode, + name: Circus.TestName, + parent: Circus.DescribeBlock, timeout: number | undefined, - asyncError: Exception, -): TestEntry => ({ + asyncError: Circus.Exception, +): Circus.TestEntry => ({ asyncError, duration: null, errors: [], @@ -76,7 +59,7 @@ export const makeTest = ( // Traverse the tree of describe blocks and return true if at least one describe // block has an enabled test. -const hasEnabledTest = (describeBlock: DescribeBlock): boolean => { +const hasEnabledTest = (describeBlock: Circus.DescribeBlock): boolean => { const {hasFocusedTests, testNamePattern} = getState(); const hasOwnEnabledTests = describeBlock.tests.some( test => @@ -90,10 +73,10 @@ const hasEnabledTest = (describeBlock: DescribeBlock): boolean => { return hasOwnEnabledTests || describeBlock.children.some(hasEnabledTest); }; -export const getAllHooksForDescribe = (describe: DescribeBlock) => { +export const getAllHooksForDescribe = (describe: Circus.DescribeBlock) => { const result: { - beforeAll: Array; - afterAll: Array; + beforeAll: Array; + afterAll: Array; } = { afterAll: [], beforeAll: [], @@ -115,12 +98,12 @@ export const getAllHooksForDescribe = (describe: DescribeBlock) => { return result; }; -export const getEachHooksForTest = (test: TestEntry) => { +export const getEachHooksForTest = (test: Circus.TestEntry) => { const result: { - beforeEach: Array; - afterEach: Array; + beforeEach: Array; + afterEach: Array; } = {afterEach: [], beforeEach: []}; - let block: DescribeBlock | undefined | null = test.parent; + let block: Circus.DescribeBlock | undefined | null = test.parent; do { const beforeEachForCurrentBlock = []; @@ -141,7 +124,9 @@ export const getEachHooksForTest = (test: TestEntry) => { return result; }; -export const describeBlockHasTests = (describe: DescribeBlock): boolean => +export const describeBlockHasTests = ( + describe: Circus.DescribeBlock, +): boolean => describe.tests.length > 0 || describe.children.some(describeBlockHasTests); const _makeTimeoutMessage = (timeout: number, isHook: boolean) => @@ -158,8 +143,8 @@ function checkIsError(error: any): error is Error { } export const callAsyncCircusFn = ( - fn: AsyncFn, - testContext: TestContext | undefined, + fn: Circus.AsyncFn, + testContext: Circus.TestContext | undefined, {isHook, timeout}: {isHook?: boolean | null; timeout: number}, ): Promise => { let timeoutID: NodeJS.Timeout; @@ -245,25 +230,27 @@ export const callAsyncCircusFn = ( }); }; -export const getTestDuration = (test: TestEntry): number | null => { +export const getTestDuration = (test: Circus.TestEntry): number | null => { const {startedAt} = test; return typeof startedAt === 'number' ? Date.now() - startedAt : null; }; export const makeRunResult = ( - describeBlock: DescribeBlock, + describeBlock: Circus.DescribeBlock, unhandledErrors: Array, -): RunResult => ({ +): Circus.RunResult => ({ testResults: makeTestResults(describeBlock), unhandledErrors: unhandledErrors.map(_formatError), }); -const makeTestResults = (describeBlock: DescribeBlock): TestResults => { +const makeTestResults = ( + describeBlock: Circus.DescribeBlock, +): Circus.TestResults => { const {includeTestLocationInResult} = getState(); - let testResults: TestResults = []; + let testResults: Circus.TestResults = []; for (const test of describeBlock.tests) { const testPath = []; - let parent: TestEntry | DescribeBlock = test; + let parent: Circus.TestEntry | Circus.DescribeBlock = test; do { testPath.unshift(parent.name); } while ((parent = parent.parent)); @@ -309,9 +296,9 @@ const makeTestResults = (describeBlock: DescribeBlock): TestResults => { // Return a string that identifies the test (concat of parent describe block // names + test title) -export const getTestID = (test: TestEntry) => { +export const getTestID = (test: Circus.TestEntry) => { const titles = []; - let parent: TestEntry | DescribeBlock = test; + let parent: Circus.TestEntry | Circus.DescribeBlock = test; do { titles.unshift(parent.name); } while ((parent = parent.parent)); @@ -321,7 +308,7 @@ export const getTestID = (test: TestEntry) => { }; const _formatError = ( - errors?: Exception | [Exception | undefined, Exception], + errors?: Circus.Exception | [Circus.Exception | undefined, Circus.Exception], ): string => { let error; let asyncError; @@ -349,9 +336,9 @@ const _formatError = ( }; export const addErrorToEachTestUnderDescribe = ( - describeBlock: DescribeBlock, - error: Exception, - asyncError: Exception, + describeBlock: Circus.DescribeBlock, + error: Circus.Exception, + asyncError: Circus.Exception, ) => { for (const test of describeBlock.tests) { test.errors.push([error, asyncError]); diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index 00fb37b8fab8..b49313c47d76 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -6,7 +6,7 @@ */ import {Script} from 'vm'; -import {Config, Global} from '@jest/types'; +import {Circus, Config, Global} from '@jest/types'; import jestMock, {ModuleMocker} from 'jest-mock'; import {ScriptTransformer} from '@jest/transform'; import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; @@ -35,6 +35,7 @@ export declare class JestEnvironment { ): {[ScriptTransformer.EVAL_RESULT_VARIABLE]: ModuleWrapper} | null; setup(): Promise; teardown(): Promise; + handleTestEvent?(event: Circus.Event, state: Circus.State): void; } export type Module = typeof module; diff --git a/packages/jest-types/src/Circus.ts b/packages/jest-types/src/Circus.ts new file mode 100644 index 000000000000..0ca639632e8d --- /dev/null +++ b/packages/jest-types/src/Circus.ts @@ -0,0 +1,218 @@ +/** + * 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 * as Global from './Global'; + +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 = Record; +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: Error; + 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: Error; + mode: BlockMode; + name: 'start_describe_definition'; + blockName: BlockName; + } + | { + mode: BlockMode; + name: 'finish_describe_definition'; + blockName: BlockName; + } + | { + asyncError: Error; + name: 'add_hook'; + hookType: HookType; + fn: HookFn; + timeout: number | undefined; + } + | { + asyncError: Error; + 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; +}; diff --git a/packages/jest-types/src/index.ts b/packages/jest-types/src/index.ts index 119377a9b79d..e38f36ad9541 100644 --- a/packages/jest-types/src/index.ts +++ b/packages/jest-types/src/index.ts @@ -5,7 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import * as Circus from './Circus'; import * as Config from './Config'; import * as Global from './Global'; -export {Config, Global}; +export {Circus, Config, Global};