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

Allow custom environments or other setup scripts to bind to Circus events. #8307

Closed
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
### Features

- `[expect]` Improve report when matcher fails, part 15 ([#8281](https://github.com/facebook/jest/pull/8281))
- `[jest-circus]` Allow Circus `addEventHandler` and `getState` to be used externally ([#8307](https://github.com/facebook/jest/pull/8307))

### Fixes

Expand Down
2 changes: 1 addition & 1 deletion packages/jest-circus/src/__mocks__/testUtils.ts
Expand Up @@ -13,7 +13,7 @@ import crypto from 'crypto';
import {sync as spawnSync, ExecaReturns} from 'execa';
import {skipSuiteOnWindows} from '@jest/test-utils';

const CIRCUS_PATH = require.resolve('../../build');
const CIRCUS_PATH = require.resolve('../../build/globals');
const CIRCUS_RUN_PATH = require.resolve('../../build/run');
const CIRCUS_STATE_PATH = require.resolve('../../build/state');
const TEST_EVENT_HANDLER_PATH = require.resolve('./testEventHandler');
Expand Down
Expand Up @@ -14,7 +14,7 @@ let circusTest: Global.It;
// the two with this alias.

const aliasCircusIt = () => {
const {it, test} = require('../');
const {it, test} = require('../globals');
circusIt = it;
circusTest = test;
};
Expand Down
Expand Up @@ -13,7 +13,7 @@ let circusIt: Global.It;
// the two with this alias.

const aliasCircusIt = () => {
const {it} = require('../');
const {it} = require('../globals');
circusIt = it;
};

Expand Down
2 changes: 1 addition & 1 deletion packages/jest-circus/src/__tests__/hooksError.test.ts
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import circus from '../';
import circus from '../globals';
import {HookType} from '../types';

describe.each(['beforeEach', 'beforeAll', 'afterEach', 'afterAll'])(
Expand Down
206 changes: 206 additions & 0 deletions packages/jest-circus/src/globals.ts
@@ -0,0 +1,206 @@
/**
* 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 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 {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',
});
const describeReturn = blockFn();

// TODO throw in Jest 25
if (isPromise(describeReturn)) {
console.log(
formatExecError(
new ErrorWithStack(
chalk.yellow(
'Returning a Promise from "describe" is not supported. Tests must be defined synchronously.\n' +
'Returning a value from "describe" will fail the test in a future version of Jest.',
),
describeFn,
),
{rootDir: '', testMatch: []},
{noStackTrace: false},
),
);
} else if (describeReturn !== undefined) {
console.log(
formatExecError(
new ErrorWithStack(
chalk.yellow(
'A "describe" callback must not return a value.\n' +
'Returning a value from "describe" will fail the test in a future version of Jest.',
),
describeFn,
),
{rootDir: '', testMatch: []},
{noStackTrace: false},
),
);
}

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<any>): 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,
};