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

feat(globals): make test return values stricter #10512

Merged
merged 3 commits into from Dec 5, 2020
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -20,6 +20,8 @@
- `[jest-circus]` Fix `testLocation` on Windows when using `test.each` ([#10871](https://github.com/facebook/jest/pull/10871))
- `[jest-console]` `console.dir` now respects the second argument correctly ([#10638](https://github.com/facebook/jest/pull/10638))
- `[jest-environment-jsdom]` Use inner realm’s `ArrayBuffer` constructor ([#10885](https://github.com/facebook/jest/pull/10885))
- `[jest-globals]` [**BREAKING**] Disallow return values other than a `Promise` from hooks and tests ([#10512](https://github.com/facebook/jest/pull/10512))
- `[jest-globals]` [**BREAKING**] Disallow mixing a done callback and returning a `Promise` from hooks and tests ([#10512](https://github.com/facebook/jest/pull/10512))
Copy link
Collaborator

@thymikee thymikee Sep 14, 2020

Choose a reason for hiding this comment

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

this actually affects jest-types only?

Copy link
Member Author

Choose a reason for hiding this comment

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

it's just a types change, yeah. The global types that everybody uses today (including us) lives in DefinitelyTyped.

- `[jest-haste-map]` Vendor `NodeWatcher` from `sane` ([#10919](https://github.com/facebook/jest/pull/10919))
- `[jest-jasmine2]` Fixed the issue of beforeAll & afterAll hooks getting executed even if it is inside a skipped `describe` block when it has child `tests` marked as either `only` or `todo` [#10451](https://github.com/facebook/jest/issues/10451)
- `[jest-jasmine2]` Fixed the issues of child `tests` marked with `only` or `todo` getting executed even if it is inside a skipped parent `describe` block [#10451](https://github.com/facebook/jest/issues/10451)
Expand Down
Expand Up @@ -83,7 +83,7 @@ export const initialize = async ({
globalsObject.test.concurrent = (test => {
const concurrent = (
testName: string,
testFn: () => Promise<unknown>,
testFn: Global.ConcurrentTestFn,
timeout?: number,
) => {
// For concurrent tests we first run the function that returns promise, and then register a
Expand All @@ -98,7 +98,7 @@ export const initialize = async ({

const only = (
testName: string,
testFn: () => Promise<unknown>,
testFn: Global.ConcurrentTestFn,
timeout?: number,
) => {
const promise = mutex(() => testFn());
Expand Down
30 changes: 16 additions & 14 deletions packages/jest-circus/src/utils.ts
Expand Up @@ -12,7 +12,7 @@ import isGeneratorFn from 'is-generator-fn';
import slash = require('slash');
import StackUtils = require('stack-utils');
import type {AssertionResult, Status} from '@jest/test-result';
import type {Circus} from '@jest/types';
import type {Circus, Global} from '@jest/types';
import {ErrorWithStack, convertDescriptorToString, formatTime} from 'jest-util';
import prettyFormat from 'pretty-format';
import {ROOT_DESCRIBE_BLOCK_NAME, getState} from './state';
Expand All @@ -21,6 +21,16 @@ const stackUtils = new StackUtils({cwd: 'A path that does not exist'});

const jestEachBuildDir = slash(path.dirname(require.resolve('jest-each')));

function takesDoneCallback(fn: Circus.AsyncFn): fn is Global.DoneTakingTestFn {
return fn.length > 0;
}

function isGeneratorFunction(
fn: Global.PromiseReturningTestFn | Global.GeneratorReturningTestFn,
): fn is Global.GeneratorReturningTestFn {
return isGeneratorFn(fn);
}

export const makeDescribe = (
name: Circus.BlockName,
parent?: Circus.DescribeBlock,
Expand Down Expand Up @@ -177,7 +187,7 @@ export const callAsyncCircusFn = (

// If this fn accepts `done` callback we return a promise that fulfills as
// soon as `done` called.
if (fn.length) {
if (takesDoneCallback(fn)) {
let returnedValue: unknown = undefined;
const done = (reason?: Error | string): void => {
// We need to keep a stack here before the promise tick
Expand Down Expand Up @@ -216,25 +226,17 @@ export const callAsyncCircusFn = (
});
};

returnedValue = fn.call<
Copy link
Member Author

@SimenB SimenB Dec 5, 2020

Choose a reason for hiding this comment

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

@villasv does any of this look wrong to you? Asking due to your work on #10836

Circus.TestContext | undefined,
Array<typeof done>,
void | Promise<unknown> | Generator | undefined
>(testContext, done);
returnedValue = fn.call(testContext, done);

return;
}

let returnedValue: any;
if (isGeneratorFn(fn)) {
let returnedValue: Global.TestReturnValue;
if (isGeneratorFunction(fn)) {
returnedValue = co.wrap(fn).call({});
} else {
try {
returnedValue = fn.call<
Circus.TestContext | undefined,
[],
void | Promise<unknown> | Generator | undefined
>(testContext);
returnedValue = fn.call(testContext);
} catch (error) {
reject(error);
return;
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-jasmine2/src/jasmineAsyncInstall.ts
Expand Up @@ -165,7 +165,7 @@ function makeConcurrent(
): Global.ItConcurrentBase {
const concurrentFn = function (
specName: string,
fn: Global.TestFn,
fn: Global.ConcurrentTestFn,
timeout?: number,
) {
let promise: Promise<unknown> = Promise.resolve();
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-types/src/Circus.ts
Expand Up @@ -20,7 +20,7 @@ export type HookFn = Global.HookFn;
export type AsyncFn = TestFn | HookFn;
export type SharedHookType = 'afterAll' | 'beforeAll';
export type HookType = SharedHookType | 'afterEach' | 'beforeEach';
export type TestContext = Record<string, unknown>;
export type TestContext = Global.TestContext;
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 = {
Expand Down
31 changes: 23 additions & 8 deletions packages/jest-types/src/Global.ts
Expand Up @@ -7,17 +7,32 @@

import type {CoverageMapData} from 'istanbul-lib-coverage';

export type GeneratorFn = (...args: Array<any>) => Generator;
export type ValidTestReturnValues = void | undefined;
type TestReturnValuePromise = Promise<unknown>;
type TestReturnValueGenerator = Generator<void, unknown, void>;
export type TestReturnValue = ValidTestReturnValues | TestReturnValuePromise;

export type TestContext = Record<string, unknown>;

export type DoneFn = (reason?: string | Error) => void;
export type CallbackFn = (
done?: DoneFn,
) => void | undefined | Promise<void | undefined | unknown>;
// these should not be undefined
export type DoneTakingTestFn = (
this: TestContext | undefined,
done: DoneFn,
) => ValidTestReturnValues;
export type PromiseReturningTestFn = (
this: TestContext | undefined,
) => TestReturnValue;
export type GeneratorReturningTestFn = (
this: TestContext | undefined,
) => TestReturnValueGenerator;

export type TestName = string;
export type TestFn = GeneratorFn | CallbackFn;
export type ConcurrentTestFn = (
done?: DoneFn,
) => Promise<void | undefined | unknown>;
export type TestFn =
| PromiseReturningTestFn
| GeneratorReturningTestFn
| DoneTakingTestFn;
export type ConcurrentTestFn = () => TestReturnValuePromise;
export type BlockFn = () => void;
export type BlockName = string;
export type HookFn = TestFn;
Expand Down
43 changes: 42 additions & 1 deletion test-types/top-level-globals.test.ts
Expand Up @@ -7,7 +7,7 @@
* @type ./empty.d.ts
*/

import {expectType} from 'mlh-tsd';
import {expectError, expectType} from 'mlh-tsd';
import {
afterAll,
afterEach,
Expand All @@ -16,8 +16,12 @@ import {
describe,
test,
} from '@jest/globals';
import type {Global} from '@jest/types';

const fn = () => {};
const doneFn: Global.DoneTakingTestFn = done => {
done();
};
const asyncFn = async () => {};
const genFn = function* () {};
const timeout = 5;
Expand All @@ -29,18 +33,55 @@ expectType<void>(afterAll(fn));
expectType<void>(afterAll(asyncFn));
expectType<void>(afterAll(genFn));
expectType<void>(afterAll(fn, timeout));
expectType<void>(afterAll(asyncFn, timeout));
expectType<void>(afterAll(genFn, timeout));
expectType<void>(afterEach(fn));
expectType<void>(afterEach(asyncFn));
expectType<void>(afterEach(genFn));
expectType<void>(afterEach(fn, timeout));
expectType<void>(afterEach(asyncFn, timeout));
expectType<void>(afterEach(genFn, timeout));
expectType<void>(beforeAll(fn));
expectType<void>(beforeAll(asyncFn));
expectType<void>(beforeAll(genFn));
expectType<void>(beforeAll(fn, timeout));
expectType<void>(beforeAll(asyncFn, timeout));
expectType<void>(beforeAll(genFn, timeout));
expectType<void>(beforeEach(fn));
expectType<void>(beforeEach(asyncFn));
expectType<void>(beforeEach(genFn));
expectType<void>(beforeEach(fn, timeout));
expectType<void>(beforeEach(asyncFn, timeout));
expectType<void>(beforeEach(genFn, timeout));

expectType<void>(test(testName, fn));
expectType<void>(test(testName, asyncFn));
expectType<void>(test(testName, doneFn));
expectType<void>(test(testName, genFn));
expectType<void>(test(testName, fn, timeout));
expectType<void>(test(testName, asyncFn, timeout));
expectType<void>(test(testName, doneFn, timeout));
expectType<void>(test(testName, genFn, timeout));

// wrong arguments
expectError(test(testName));
expectError(test(testName, timeout));
expectError(test(timeout, fn));

// wrong return value
expectError(test(testName, () => 42));

// mixing done callback and promise/generator
expectError(
test(testName, async done => {
done();
}),
);
expectError(
test(testName, function* (done) {
done();
}),
);

expectType<void>(test(testName, fn));
expectType<void>(test(testName, asyncFn));
Expand Down