0
# Custom Test Runner Framework
1
2
Abstract base classes and interfaces for building custom test runners with both callback and event-driven patterns. This framework enables extending Jest's test execution capabilities with custom test runners for different testing scenarios.
3
4
## Imports
5
6
```typescript
7
import {
8
CallbackTestRunner,
9
EmittingTestRunner,
10
type CallbackTestRunnerInterface,
11
type EmittingTestRunnerInterface,
12
type OnTestStart,
13
type OnTestFailure,
14
type OnTestSuccess,
15
type UnsubscribeFn
16
} from "jest-runner";
17
```
18
19
## Capabilities
20
21
### CallbackTestRunner Abstract Class
22
23
Base class for building custom test runners using callback-style interfaces for test lifecycle events.
24
25
```typescript { .api }
26
/**
27
* Abstract base class for callback-style test runners
28
* Suitable for runners that prefer explicit callback handling over events
29
*/
30
abstract class CallbackTestRunner extends BaseTestRunner implements CallbackTestRunnerInterface {
31
readonly supportsEventEmitters = false;
32
readonly isSerial?: boolean;
33
34
constructor(globalConfig: Config.GlobalConfig, context: TestRunnerContext);
35
36
/**
37
* Execute tests using callback-style event handling
38
* @param tests - Array of test objects to execute
39
* @param watcher - Test watcher for interrupt handling
40
* @param onStart - Callback invoked when each test starts
41
* @param onResult - Callback invoked when each test succeeds
42
* @param onFailure - Callback invoked when each test fails
43
* @param options - Execution options including serial mode
44
*/
45
abstract runTests(
46
tests: Array<Test>,
47
watcher: TestWatcher,
48
onStart: OnTestStart,
49
onResult: OnTestSuccess,
50
onFailure: OnTestFailure,
51
options: TestRunnerOptions
52
): Promise<void>;
53
}
54
```
55
56
**Usage Example:**
57
58
```typescript
59
import { CallbackTestRunner } from "jest-runner";
60
import type { Test, TestResult, SerializableError } from "@jest/test-result";
61
62
class CustomCallbackRunner extends CallbackTestRunner {
63
async runTests(
64
tests: Array<Test>,
65
watcher: TestWatcher,
66
onStart: OnTestStart,
67
onResult: OnTestSuccess,
68
onFailure: OnTestFailure,
69
options: TestRunnerOptions
70
): Promise<void> {
71
for (const test of tests) {
72
if (watcher.isInterrupted()) break;
73
74
try {
75
await onStart(test);
76
77
// Custom test execution logic
78
const result = await this.executeCustomTest(test);
79
80
await onResult(test, result);
81
} catch (error) {
82
await onFailure(test, this.formatError(error));
83
}
84
}
85
}
86
87
private async executeCustomTest(test: Test): Promise<TestResult> {
88
// Custom test execution implementation
89
return {
90
numPassingTests: 1,
91
numFailingTests: 0,
92
numPendingTests: 0,
93
numTodoTests: 0,
94
testFilePath: test.path,
95
// ... other TestResult properties
96
};
97
}
98
}
99
```
100
101
### EmittingTestRunner Abstract Class
102
103
Base class for building custom test runners using event-driven interfaces for real-time test progress reporting.
104
105
```typescript { .api }
106
/**
107
* Abstract base class for event-emitting test runners
108
* Suitable for runners that need real-time event broadcasting
109
*/
110
abstract class EmittingTestRunner extends BaseTestRunner implements EmittingTestRunnerInterface {
111
readonly supportsEventEmitters = true;
112
readonly isSerial?: boolean;
113
114
constructor(globalConfig: Config.GlobalConfig, context: TestRunnerContext);
115
116
/**
117
* Execute tests using event-driven progress reporting
118
* @param tests - Array of test objects to execute
119
* @param watcher - Test watcher for interrupt handling
120
* @param options - Execution options including serial mode
121
*/
122
abstract runTests(
123
tests: Array<Test>,
124
watcher: TestWatcher,
125
options: TestRunnerOptions
126
): Promise<void>;
127
128
/**
129
* Register event listener for test execution events
130
* @param eventName - Name of the event to listen for
131
* @param listener - Function to call when event is emitted
132
* @returns Unsubscribe function to remove the listener
133
*/
134
abstract on<Name extends keyof TestEvents>(
135
eventName: Name,
136
listener: (eventData: TestEvents[Name]) => void | Promise<void>
137
): UnsubscribeFn;
138
}
139
```
140
141
**Usage Example:**
142
143
```typescript
144
import { EmittingTestRunner } from "jest-runner";
145
import Emittery from "emittery";
146
147
class CustomEmittingRunner extends EmittingTestRunner {
148
private eventEmitter = new Emittery<TestEvents>();
149
150
async runTests(
151
tests: Array<Test>,
152
watcher: TestWatcher,
153
options: TestRunnerOptions
154
): Promise<void> {
155
for (const test of tests) {
156
if (watcher.isInterrupted()) break;
157
158
try {
159
await this.eventEmitter.emit('test-file-start', [test]);
160
161
// Custom test execution logic
162
const result = await this.executeCustomTest(test);
163
164
await this.eventEmitter.emit('test-file-success', [test, result]);
165
} catch (error) {
166
await this.eventEmitter.emit('test-file-failure', [test, error]);
167
}
168
}
169
}
170
171
on<Name extends keyof TestEvents>(
172
eventName: Name,
173
listener: (eventData: TestEvents[Name]) => void | Promise<void>
174
): UnsubscribeFn {
175
return this.eventEmitter.on(eventName, listener);
176
}
177
}
178
```
179
180
### Test Runner Interfaces
181
182
Type definitions for implementing custom test runners without extending the abstract base classes.
183
184
```typescript { .api }
185
/**
186
* Interface for callback-style test runners
187
*/
188
interface CallbackTestRunnerInterface {
189
readonly isSerial?: boolean;
190
readonly supportsEventEmitters?: boolean;
191
192
runTests(
193
tests: Array<Test>,
194
watcher: TestWatcher,
195
onStart: OnTestStart,
196
onResult: OnTestSuccess,
197
onFailure: OnTestFailure,
198
options: TestRunnerOptions
199
): Promise<void>;
200
}
201
202
/**
203
* Interface for event-emitting test runners
204
*/
205
interface EmittingTestRunnerInterface {
206
readonly isSerial?: boolean;
207
readonly supportsEventEmitters: true;
208
209
runTests(
210
tests: Array<Test>,
211
watcher: TestWatcher,
212
options: TestRunnerOptions
213
): Promise<void>;
214
215
on<Name extends keyof TestEvents>(
216
eventName: Name,
217
listener: (eventData: TestEvents[Name]) => void | Promise<void>
218
): UnsubscribeFn;
219
}
220
```
221
222
### Runner Implementation Examples
223
224
Complete examples showing how to implement both callback and event-driven test runners.
225
226
**Callback Runner Implementation:**
227
228
```typescript
229
class ESLintTestRunner extends CallbackTestRunner {
230
async runTests(
231
tests: Array<Test>,
232
watcher: TestWatcher,
233
onStart: OnTestStart,
234
onResult: OnTestSuccess,
235
onFailure: OnTestFailure,
236
options: TestRunnerOptions
237
): Promise<void> {
238
// Access inherited properties
239
const { collectCoverage } = this._globalConfig;
240
const { changedFiles } = this._context;
241
242
for (const test of tests) {
243
if (watcher.isInterrupted()) break;
244
245
await onStart(test);
246
247
try {
248
// Run ESLint on the test file
249
const results = await this.runESLint(test.path);
250
251
const testResult: TestResult = {
252
numFailingTests: results.errorCount,
253
numPassingTests: results.errorCount === 0 ? 1 : 0,
254
numPendingTests: 0,
255
numTodoTests: 0,
256
testFilePath: test.path,
257
console: [],
258
leaks: false,
259
// Map ESLint messages to test output
260
testResults: results.messages.map(msg => ({
261
title: `ESLint: ${msg.ruleId || 'Error'}`,
262
status: msg.severity === 2 ? 'failed' : 'passed',
263
ancestorTitles: [],
264
fullName: `${test.path}:${msg.line}:${msg.column}`,
265
})),
266
};
267
268
await onResult(test, testResult);
269
} catch (error) {
270
await onFailure(test, {
271
message: error.message,
272
stack: error.stack,
273
type: error.constructor.name,
274
});
275
}
276
}
277
}
278
}
279
```
280
281
**Event-Driven Runner Implementation:**
282
283
```typescript
284
class PuppeteerTestRunner extends EmittingTestRunner {
285
private eventEmitter = new Emittery<TestEvents>();
286
287
async runTests(
288
tests: Array<Test>,
289
watcher: TestWatcher,
290
options: TestRunnerOptions
291
): Promise<void> {
292
const browser = await puppeteer.launch();
293
294
try {
295
for (const test of tests) {
296
if (watcher.isInterrupted()) break;
297
298
await this.eventEmitter.emit('test-file-start', [test]);
299
300
try {
301
const page = await browser.newPage();
302
const result = await this.runBrowserTest(test, page);
303
await page.close();
304
305
await this.eventEmitter.emit('test-file-success', [test, result]);
306
} catch (error) {
307
await this.eventEmitter.emit('test-file-failure', [test, error]);
308
}
309
}
310
} finally {
311
await browser.close();
312
}
313
}
314
315
on<Name extends keyof TestEvents>(
316
eventName: Name,
317
listener: (eventData: TestEvents[Name]) => void | Promise<void>
318
): UnsubscribeFn {
319
return this.eventEmitter.on(eventName, listener);
320
}
321
}
322
```
323
324
## Types
325
326
```typescript { .api }
327
type OnTestStart = (test: Test) => Promise<void>;
328
type OnTestFailure = (test: Test, serializableError: SerializableError) => Promise<void>;
329
type OnTestSuccess = (test: Test, testResult: TestResult) => Promise<void>;
330
type UnsubscribeFn = () => void;
331
type JestTestRunner = CallbackTestRunner | EmittingTestRunner;
332
333
abstract class BaseTestRunner {
334
readonly isSerial?: boolean;
335
abstract readonly supportsEventEmitters: boolean;
336
337
protected readonly _globalConfig: Config.GlobalConfig;
338
protected readonly _context: TestRunnerContext;
339
340
constructor(globalConfig: Config.GlobalConfig, context: TestRunnerContext);
341
}
342
```