Skip to content
Merged
24 changes: 11 additions & 13 deletions packages/jest-core/src/TestScheduler.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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>();

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
Original file line number Diff line number Diff line change
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,
) {}
}

export abstract class TestRunner extends BaseTestRunner {
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;
}

export type JestTestRunner = TestRunner | EmittingTestRunner;