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
41 changes: 15 additions & 26 deletions 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/');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need to add / after build

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll resolve to the index.js path. It could probably be without build as well, and it'll use main

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding the / wasn't strictly necessary, it's just a result of churn because I changed the import path to something else and then back again

doesn't make a difference either way, it'll resolve to index

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 All @@ -36,6 +36,7 @@ export const runTest = (source: string) => {
const content = `
require('${BABEL_REGISTER_PATH}')({extensions: [".js", ".ts"]});
const circus = require('${CIRCUS_PATH}');
global.it = circus.it;
global.test = circus.test;
global.describe = circus.describe;
global.beforeEach = circus.beforeEach;
Expand All @@ -55,35 +56,23 @@ export const runTest = (source: string) => {
`;

fs.writeFileSync(tmpFilename, content);
const result = spawnSync('node', [tmpFilename], {
cwd: process.cwd(),
}) as Result;

// For compat with cross-spawn
result.status = result.code;
// Normalize for compat with cross-spawn
let result: Result;

if (result.status !== 0) {
const message = `
STDOUT: ${result.stdout && result.stdout.toString()}
STDERR: ${result.stderr && result.stderr.toString()}
STATUS: ${result.status}
ERROR: ${String(result.error)}
`;
throw new Error(message);
try {
result = spawnSync('node', [tmpFilename], {
cwd: process.cwd(),
}) as Result;
} catch (err) {
result = err;
}

result.stdout = String(result.stdout);
result.stderr = String(result.stderr);

fs.unlinkSync(tmpFilename);

if (result.stderr) {
throw new Error(
`
Unexpected stderr:
${result.stderr}
`,
);
}
return result;
return {
status: result.code,
stderr: String(result.stderr),
stdout: String(result.stdout),
};
};
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`beforeAll is exectued correctly 1`] = `
exports[`beforeAll is executed correctly 1`] = `
"start_describe_definition: describe 1
add_hook: beforeAll
add_test: test 1
Expand Down
79 changes: 19 additions & 60 deletions packages/jest-circus/src/__tests__/circusItTestError.test.ts
Expand Up @@ -5,75 +5,34 @@
* LICENSE file in the root directory of this source tree.
*/

import {Global} from '@jest/types';
import {runTest} from '../__mocks__/testUtils';

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('../');
circusIt = it;
circusTest = test;
};

aliasCircusIt();

describe('test/it error throwing', () => {
it(`it doesn't throw an error with valid arguments`, () => {
expect(() => {
circusIt('test1', () => {});
}).not.toThrowError();
describe('error throwing', () => {
test(`doesn't throw an error with valid arguments`, () => {
expect(runTest(`test('test1', () => {});`).stderr).toEqual('');
});
it(`it throws error with missing callback function`, () => {
expect(() => {
// @ts-ignore: Easy, we're testing runtime errors here
circusIt('test2');
}).toThrowError(

test(`throws error with missing callback function`, () => {
expect(runTest(`test('test2');`).stderr).toContain(
'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.',
);
});
it(`it throws an error when first argument isn't a string`, () => {
expect(() => {
// @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(() => {
// @ts-ignore: Easy, we're testing runtime errors here
circusIt('test4', 'test4b');
}).toThrowError(
'Invalid second argument, test4b. It must be a callback function.',

test(`throws an error when first argument isn't a string`, () => {
expect(runTest(`test(() => {});`).stderr).toContain(
'Invalid first argument, () => {}. It must be a string.',
);
});
it(`test doesn't throw an error with valid arguments`, () => {
expect(() => {
circusTest('test5', () => {});
}).not.toThrowError();
});
it(`test throws error with missing callback function`, () => {
expect(() => {
// @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.',

test(`throws an error when callback function is not a function`, () => {
expect(runTest(`test('test4', 'test4b');`).stderr).toContain(
'Invalid second argument, test4b. It must be a callback function.',
);
});
it(`test throws an error when first argument isn't a string`, () => {
expect(() => {
// @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(() => {
// @ts-ignore: Easy, we're testing runtime errors here
circusTest('test8', 'test8b');
}).toThrowError(
'Invalid second argument, test8b. It must be a callback function.',

test(`doesn't throw an error with valid arguments`, () => {
expect(runTest(`test('test4', 'test4b');`).stderr).toContain(
'Invalid second argument, test4b. It must be a callback function.',
);
});
});
2 changes: 1 addition & 1 deletion packages/jest-circus/src/__tests__/hooks.test.ts
Expand Up @@ -56,7 +56,7 @@ test('multiple before each hooks in one describe are executed in the right order
expect(stdout).toMatchSnapshot();
});

test('beforeAll is exectued correctly', () => {
test('beforeAll is executed correctly', () => {
const {stdout} = runTest(`
describe('describe 1', () => {
beforeAll(() => console.log('> beforeAll 1'));
Expand Down
196 changes: 196 additions & 0 deletions packages/jest-circus/src/globals.ts
@@ -0,0 +1,196 @@
/**
* 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;

export 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.
export const beforeEach: THook = (fn, timeout) =>
_addHook(fn, 'beforeEach', beforeEach, timeout);
export const beforeAll: THook = (fn, timeout) =>
_addHook(fn, 'beforeAll', beforeAll, timeout);
export const afterEach: THook = (fn, timeout) =>
_addHook(fn, 'afterEach', afterEach, timeout);
export const afterAll: THook = (fn, timeout) =>
_addHook(fn, 'afterAll', afterAll, timeout);

export 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;
})();

export const it: Global.It = test;