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
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
6 changes: 0 additions & 6 deletions packages/jest-runner/src/__tests__/testRunner.test.ts
Expand Up @@ -42,9 +42,6 @@ test('injects the serializable module map into each worker in watch mode', async
{context, path: './file2.test.js'},
],
new TestWatcher({isWatchMode: globalConfig.watch}),
undefined,
undefined,
undefined,
{serial: false},
);

Expand Down Expand Up @@ -76,9 +73,6 @@ test('assign process.env.JEST_WORKER_ID = 1 when in runInBand mode', async () =>
await new TestRunner(globalConfig, {}).runTests(
[{context, path: './file.test.js'}],
new TestWatcher({isWatchMode: globalConfig.watch}),
undefined,
undefined,
undefined,
{serial: true},
);

Expand Down
52 changes: 17 additions & 35 deletions packages/jest-runner/src/index.ts
Expand Up @@ -14,19 +14,11 @@ import type {
TestFileEvent,
TestResult,
} from '@jest/test-result';
import type {Config} from '@jest/types';
import {deepCyclicCopy} from 'jest-util';
import {PromiseWithCustomMessage, Worker} from 'jest-worker';
import runTest from './runTest';
import type {SerializableResolver, worker} from './testWorker';
import type {
OnTestFailure,
OnTestStart,
OnTestSuccess,
TestRunnerContext,
TestRunnerOptions,
TestWatcher,
} from './types';
import {EmittingTestRunner, TestRunnerOptions, TestWatcher} from './types';

const TEST_WORKER_PATH = require.resolve('./testWorker');

Expand All @@ -41,29 +33,17 @@ export type {
TestWatcher,
TestRunnerContext,
TestRunnerOptions,
EmittingTestRunner,
JestTestRunner,
TestRunner,
} from './types';

export default class TestRunner {
private readonly _globalConfig: Config.GlobalConfig;
private readonly _context: TestRunnerContext;
private readonly eventEmitter = new Emittery<TestEvents>();
readonly supportsEventEmitters: boolean = true;

readonly isSerial?: boolean;

constructor(globalConfig: Config.GlobalConfig, context: TestRunnerContext) {
this._globalConfig = globalConfig;
this._context = context;
}
export default class TestRunner extends EmittingTestRunner {
private readonly _eventEmitter = new Emittery<TestEvents>();
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

async runTests(
tests: Array<Test>,
watcher: TestWatcher,
// keep these three as they're still passed and should be in the types,
// even if this particular runner doesn't use them
_onStart: OnTestStart | undefined,
_onResult: OnTestSuccess | undefined,
_onFailure: OnTestFailure | undefined,
options: TestRunnerOptions,
): Promise<void> {
return await (options.serial
Expand All @@ -85,12 +65,12 @@ export default class TestRunner {

// `deepCyclicCopy` used here to avoid mem-leak
const sendMessageToJest: TestFileEvent = (eventName, args) =>
this.eventEmitter.emit(
this._eventEmitter.emit(
eventName,
deepCyclicCopy(args, {keepPrototype: false}),
);

await this.eventEmitter.emit('test-file-start', [test]);
await this._eventEmitter.emit('test-file-start', [test]);

return runTest(
test.path,
Expand All @@ -103,8 +83,9 @@ export default class TestRunner {
})
.then(
result =>
this.eventEmitter.emit('test-file-success', [test, result]),
err => this.eventEmitter.emit('test-file-failure', [test, err]),
this._eventEmitter.emit('test-file-success', [test, result]),
error =>
this._eventEmitter.emit('test-file-failure', [test, error]),
),
),
Promise.resolve(),
Expand Down Expand Up @@ -146,7 +127,7 @@ export default class TestRunner {
return Promise.reject();
}

await this.eventEmitter.emit('test-file-start', [test]);
await this._eventEmitter.emit('test-file-start', [test]);

const promise = worker.worker({
config: test.context.config,
Expand All @@ -166,7 +147,7 @@ export default class TestRunner {
if (promise.UNSTABLE_onCustomMessage) {
// TODO: Get appropriate type for `onCustomMessage`
promise.UNSTABLE_onCustomMessage(([event, payload]: any) =>
this.eventEmitter.emit(event, payload),
this._eventEmitter.emit(event, payload),
);
}

Expand All @@ -184,8 +165,9 @@ export default class TestRunner {
const runAllTests = Promise.all(
tests.map(test =>
runTestInWorker(test).then(
result => this.eventEmitter.emit('test-file-success', [test, result]),
error => this.eventEmitter.emit('test-file-failure', [test, error]),
result =>
this._eventEmitter.emit('test-file-success', [test, result]),
error => this._eventEmitter.emit('test-file-failure', [test, error]),
),
),
);
Expand All @@ -211,7 +193,7 @@ export default class TestRunner {
eventName: Name,
listener: (eventData: TestEvents[Name]) => void | Promise<void>,
): Emittery.UnsubscribeFn {
return this.eventEmitter.on(eventName, listener);
return this._eventEmitter.on(eventName, listener);
}
}

Expand Down
43 changes: 43 additions & 0 deletions packages/jest-runner/src/types.ts
Expand Up @@ -10,6 +10,7 @@ import type {JestEnvironment} from '@jest/environment';
import type {
SerializableError,
Test,
TestEvents,
TestFileEvent,
TestResult,
} from '@jest/test-result';
Expand All @@ -19,10 +20,12 @@ import type RuntimeType from 'jest-runtime';
export type ErrorWithCode = Error & {code?: string};

export type OnTestStart = (test: Test) => Promise<void>;

export type OnTestFailure = (
test: Test,
serializableError: SerializableError,
) => Promise<void>;

export type OnTestSuccess = (
test: Test,
testResult: TestResult,
Expand Down Expand Up @@ -60,3 +63,43 @@ export interface TestWatcher extends Emittery<{change: WatcherState}> {
isInterrupted(): boolean;
isWatchMode(): boolean;
}

abstract class BaseTestRunner {
readonly isSerial?: boolean;
abstract readonly supportsEventEmitters: boolean;

constructor(
protected readonly _globalConfig: Config.GlobalConfig,
protected readonly _context: TestRunnerContext,
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
) {}
}

export abstract class TestRunner extends BaseTestRunner {
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
readonly supportsEventEmitters = false;

abstract runTests(
tests: Array<Test>,
watcher: TestWatcher,
onStart: OnTestStart,
onResult: OnTestSuccess,
onFailure: OnTestFailure,
options: TestRunnerOptions,
): Promise<void>;
}

export abstract class EmittingTestRunner extends BaseTestRunner {
readonly supportsEventEmitters = true;

abstract runTests(
tests: Array<Test>,
watcher: TestWatcher,
options: TestRunnerOptions,
): Promise<void>;

abstract on<Name extends keyof TestEvents>(
eventName: Name,
listener: (eventData: TestEvents[Name]) => void | Promise<void>,
): Emittery.UnsubscribeFn;
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
}

export type JestTestRunner = TestRunner | EmittingTestRunner;