Skip to content

Commit

Permalink
Bind to Circus events via an optional event handler on any custom env. (
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
scotthovestadt committed Apr 19, 2019
1 parent cd415e7 commit 31e06e8
Show file tree
Hide file tree
Showing 21 changed files with 459 additions and 339 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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))
Expand Down
8 changes: 8 additions & 0 deletions docs/Configuration.md
Expand Up @@ -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._
Expand Down Expand Up @@ -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;
Expand Down
46 changes: 46 additions & 0 deletions 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",
]
`);
});
13 changes: 13 additions & 0 deletions 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;
@@ -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);
});
5 changes: 5 additions & 0 deletions e2e/test-environment-circus/package.json
@@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}
19 changes: 19 additions & 0 deletions packages/jest-circus/README.md
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions packages/jest-circus/src/__mocks__/testEventHandler.ts
Expand Up @@ -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': {
Expand Down
5 changes: 3 additions & 2 deletions packages/jest-circus/src/eventHandler.ts
Expand Up @@ -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,
Expand All @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions packages/jest-circus/src/formatNodeAssertErrors.ts
Expand Up @@ -6,6 +6,7 @@
*/

import {AssertionError} from 'assert';
import {Circus} from '@jest/types';
import {
diff,
printExpected,
Expand All @@ -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;
Expand All @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions packages/jest-circus/src/globalErrorHandlers.ts
Expand Up @@ -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) => {
Expand All @@ -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');
Expand All @@ -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);
Expand Down
77 changes: 43 additions & 34 deletions packages/jest-circus/src/index.ts
Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -102,8 +95,8 @@ const _dispatchDescribe = (
};

const _addHook = (
fn: HookFn,
hookType: HookType,
fn: Circus.HookFn,
hookType: Circus.HookType,
hookFn: THook,
timeout?: number,
) => {
Expand All @@ -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<any>): 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<any>): void => {
if (rest.length > 0 || typeof testName !== 'string') {
throw new ErrorWithStack(
'Todo must be called with only a description.',
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
Expand Up @@ -39,6 +39,7 @@ const jestAdapter = async (

const {globals, snapshotState} = initialize({
config,
environment,
getBabelTraverse,
getPrettier,
globalConfig,
Expand Down

0 comments on commit 31e06e8

Please sign in to comment.