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(jest-runner): improve typings by exposing TestRunner abstract classes #12646

Merged
merged 17 commits into from Apr 8, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -42,6 +42,7 @@
- `[jest-resolve, jest-runtime]` Add support for async resolver ([#11540](https://github.com/facebook/jest/pull/11540))
- `[jest-runner]` Allow `setupFiles` module to export an async function ([#12042](https://github.com/facebook/jest/pull/12042))
- `[jest-runner]` Allow passing `testEnvironmentOptions` via docblocks ([#12470](https://github.com/facebook/jest/pull/12470))
- `[jest-runner]` Exposing `CallbackTestRunner`, `EmittingTestRunner` abstract classes to help typing third party runners ([#12646](https://github.com/facebook/jest/pull/12646))
- `[jest-runtime]` [**BREAKING**] `Runtime.createHasteMap` now returns a promise ([#12008](https://github.com/facebook/jest/pull/12008))
- `[jest-runtime]` Calling `jest.resetModules` function will clear FS and transform cache ([#12531](https://github.com/facebook/jest/pull/12531))
- `[@jest/schemas]` New module for JSON schemas for Jest's config ([#12384](https://github.com/facebook/jest/pull/12384))
Expand Down
24 changes: 11 additions & 13 deletions packages/jest-core/src/TestScheduler.ts
Expand Up @@ -29,7 +29,7 @@ import {
import {createScriptTransformer} from '@jest/transform';
import type {Config} from '@jest/types';
import {formatExecError} from 'jest-message-util';
import type TestRunner from 'jest-runner';
import type {JestTestRunner, TestRunnerContext} from 'jest-runner';
import type {Context} from 'jest-runtime';
import {
buildSnapshotResolver,
Expand All @@ -40,6 +40,11 @@ import ReporterDispatcher from './ReporterDispatcher';
import type TestWatcher from './TestWatcher';
import {shouldRunInBand} from './testSchedulerHelper';

type TestRunnerConstructor = new (
globalConfig: Config.GlobalConfig,
context: TestRunnerContext,
) => JestTestRunner;

export type TestSchedulerOptions = {
startRun: (globalConfig: Config.GlobalConfig) => void;
};
Expand Down Expand Up @@ -206,14 +211,14 @@ class TestScheduler {
showStatus: !runInBand,
});

const testRunners: {[key: string]: TestRunner} = Object.create(null);
const contextsByTestRunner = new WeakMap<TestRunner, Context>();
const testRunners: Record<string, JestTestRunner> = Object.create(null);
const contextsByTestRunner = new WeakMap<JestTestRunner, Context>();
await Promise.all(
Array.from(contexts).map(async context => {
const {config} = context;
if (!testRunners[config.runner]) {
const transformer = await createScriptTransformer(config);
const Runner: typeof TestRunner =
const Runner: TestRunnerConstructor =
await transformer.requireAndTranspileModule(config.runner);
const runner = new Runner(this._globalConfig, {
changedFiles: this._context.changedFiles,
Expand Down Expand Up @@ -262,14 +267,7 @@ class TestScheduler {
),
];

await testRunner.runTests(
tests,
watcher,
undefined,
undefined,
undefined,
testRunnerOptions,
);
await testRunner.runTests(tests, watcher, testRunnerOptions);

unsubscribes.forEach(sub => sub());
} else {
Expand Down Expand Up @@ -310,7 +308,7 @@ class TestScheduler {
}

private _partitionTests(
testRunners: Record<string, TestRunner>,
testRunners: Record<string, JestTestRunner>,
tests: Array<Test>,
): Record<string, Array<Test>> | null {
if (Object.keys(testRunners).length > 1) {
Expand Down
69 changes: 69 additions & 0 deletions packages/jest-runner/__typetests__/jest-runner.test.ts
@@ -0,0 +1,69 @@
/**
* 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 type {UnsubscribeFn} from 'emittery';
import {expectType} from 'tsd-lite';
import type {Test, TestEvents} from '@jest/test-result';
import type {Config} from '@jest/types';
import {CallbackTestRunner, EmittingTestRunner} from 'jest-runner';
import type {
OnTestFailure,
OnTestStart,
OnTestSuccess,
TestRunnerContext,
TestRunnerOptions,
TestWatcher,
} from 'jest-runner';

const globalConfig = {} as Config.GlobalConfig;
const runnerContext = {} as TestRunnerContext;

class CallbackRunner extends CallbackTestRunner {
async runTests(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was hoping that override here in a test could catch some drift (hence #12648). Apparently override has no effect in this case, because methods are marked abstract.

In the other hand, abstract requires to implement those methods in a derived class. So to have this test passing is still good.

tests: Array<Test>,
watcher: TestWatcher,
onStart: OnTestStart,
onResult: OnTestSuccess,
onFailure: OnTestFailure,
options: TestRunnerOptions,
): Promise<void> {
expectType<Config.GlobalConfig>(this._globalConfig);
expectType<TestRunnerContext>(this._context);

return;
}
}

const callbackRunner = new CallbackRunner(globalConfig, runnerContext);

expectType<boolean | undefined>(callbackRunner.isSerial);
expectType<false>(callbackRunner.supportsEventEmitters);

class EmittingRunner extends EmittingTestRunner {
async runTests(
tests: Array<Test>,
watcher: TestWatcher,
options: TestRunnerOptions,
): Promise<void> {
expectType<Config.GlobalConfig>(this._globalConfig);
expectType<TestRunnerContext>(this._context);

return;
}

on<Name extends keyof TestEvents>(
eventName: string,
listener: (eventData: TestEvents[Name]) => void | Promise<void>,
): UnsubscribeFn {
return () => {};
}
}

const emittingRunner = new EmittingRunner(globalConfig, runnerContext);

expectType<boolean | undefined>(emittingRunner.isSerial);
expectType<true>(emittingRunner.supportsEventEmitters);
11 changes: 11 additions & 0 deletions packages/jest-runner/__typetests__/tsconfig.json
@@ -0,0 +1,11 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"noUnusedLocals": false,
"noUnusedParameters": false,
"skipLibCheck": true,

"types": []
},
"include": ["./**/*"]
}
3 changes: 2 additions & 1 deletion packages/jest-runner/package.json
Expand Up @@ -39,10 +39,11 @@
"throat": "^6.0.1"
},
"devDependencies": {
"@tsd/typescript": "~4.6.2",
"@types/exit": "^0.1.30",
"@types/graceful-fs": "^4.1.2",
"@types/source-map-support": "^0.5.0",
"jest-jasmine2": "^28.0.0-alpha.8"
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
"tsd-lite": "^0.5.1"
},
"engines": {
"node": "^12.13.0 || ^14.15.0 || ^16.13.0 || >=17.0.0"
Expand Down
55 changes: 23 additions & 32 deletions packages/jest-runner/src/__tests__/testRunner.test.ts
Expand Up @@ -7,6 +7,7 @@
*/

import {TestWatcher} from '@jest/core';
import type {TestContext} from '@jest/test-result';
import {makeGlobalConfig, makeProjectConfig} from '@jest/test-utils';
import TestRunner from '../index';

Expand All @@ -29,56 +30,46 @@ jest.mock('../testWorker', () => {});
test('injects the serializable module map into each worker in watch mode', async () => {
const globalConfig = makeGlobalConfig({maxWorkers: 2, watch: true});
const config = makeProjectConfig({rootDir: '/path/'});
const serializableModuleMap = jest.fn();
const runContext = {};
const context = {
const mockTestContext = {
config,
moduleMap: {toJSON: () => serializableModuleMap},
};
moduleMap: {toJSON: jest.fn()},
} as unknown as TestContext;

await new TestRunner(globalConfig, {}).runTests(
await new TestRunner(globalConfig, runContext).runTests(
[
{context, path: './file.test.js'},
{context, path: './file2.test.js'},
{context: mockTestContext, path: './file.test.js'},
{context: mockTestContext, path: './file2.test.js'},
],
new TestWatcher({isWatchMode: globalConfig.watch}),
undefined,
undefined,
undefined,
{serial: false},
);

expect(mockWorkerFarm.worker.mock.calls).toEqual([
[
{
config,
context: runContext,
globalConfig,
path: './file.test.js',
},
],
[
{
config,
context: runContext,
globalConfig,
path: './file2.test.js',
},
],
]);
expect(mockWorkerFarm.worker).toBeCalledTimes(2);

expect(mockWorkerFarm.worker).nthCalledWith(1, {
config,
context: runContext,
globalConfig,
path: './file.test.js',
});

expect(mockWorkerFarm.worker).nthCalledWith(2, {
config,
context: runContext,
globalConfig,
path: './file2.test.js',
});
});

test('assign process.env.JEST_WORKER_ID = 1 when in runInBand mode', async () => {
const globalConfig = makeGlobalConfig({maxWorkers: 1, watch: false});
const config = makeProjectConfig({rootDir: '/path/'});
const context = {config};
const context = {config} as TestContext;

await new TestRunner(globalConfig, {}).runTests(
[{context, path: './file.test.js'}],
new TestWatcher({isWatchMode: globalConfig.watch}),
undefined,
undefined,
undefined,
{serial: true},
);

Expand Down