Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bind to Circus events via an optional event handler on any custom env. #8344

Merged
merged 7 commits into from Apr 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) {
SimenB marked this conversation as resolved.
Show resolved Hide resolved
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