Skip to content

Commit e86fc39

Browse files
feat: support afterAll hook (#387)
1 parent e886cc5 commit e86fc39

7 files changed

Lines changed: 159 additions & 6 deletions

File tree

packages/dom-evaluator/src/dom-test-evaluator.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { assert as chaiAssert } from "chai";
77
import type {
88
TestEvaluator,
99
Fail,
10+
CodeEvent,
1011
TestEvent,
1112
InitEvent,
1213
Pass,
@@ -182,8 +183,18 @@ ${rawTest}`);
182183
return this.#runTest!(test);
183184
}
184185

186+
async runCode(code: string) {
187+
try {
188+
await eval(code);
189+
} catch (err) {
190+
// If the code throws an error, we want to log it to the console
191+
// so that it can be debugged.
192+
console.error("Error evaluating code:", code, err);
193+
}
194+
}
195+
185196
async handleMessage(
186-
e: TestEvent | InitEvent<InitTestFrameOptions>,
197+
e: CodeEvent | TestEvent | InitEvent<InitTestFrameOptions>,
187198
): Promise<void> {
188199
if (e.data.type === "test") {
189200
const result = await this.#runTest!(e.data.value);
@@ -192,6 +203,10 @@ ${rawTest}`);
192203
} else if (e.data.type === "init") {
193204
await this.init(e.data.value);
194205
self.parent.postMessage(READY_MESSAGE, "*");
206+
} else if (e.data.type === "code") {
207+
// This is used to run arbitrary non-test code, such as the afterAll hook.
208+
await this.runCode(e.data.value);
209+
e.ports[0].postMessage({ type: "code" });
195210
}
196211
}
197212
}

packages/javascript-evaluator/src/javascript-test-evaluator.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
Fail,
99
InitEvent,
1010
TestEvent,
11+
CodeEvent,
1112
InitWorkerOptions,
1213
} from "../../shared/src/interfaces/test-evaluator";
1314
import type { ReadyEvent } from "../../shared/src/interfaces/test-runner";
@@ -107,8 +108,18 @@ ${test};`);
107108
return this.#runTest!(test);
108109
}
109110

111+
async runCode(code: string) {
112+
try {
113+
await eval(code);
114+
} catch (err) {
115+
// If the code throws an error, we want to log it to the console
116+
// so that it can be debugged.
117+
console.error("Error evaluating code:", code, err);
118+
}
119+
}
120+
110121
async handleMessage(
111-
e: TestEvent | InitEvent<InitWorkerOptions>,
122+
e: TestEvent | InitEvent<InitWorkerOptions> | CodeEvent,
112123
): Promise<void> {
113124
if (e.data.type === "test") {
114125
const result = await this.#runTest!(e.data.value);
@@ -117,12 +128,16 @@ ${test};`);
117128
} else if (e.data.type === "init") {
118129
this.init(e.data.value);
119130
postMessage(READY_MESSAGE);
131+
} else if (e.data.type === "code") {
132+
// This is used to run arbitrary non-test code, such as the afterAll hook.
133+
await this.runCode(e.data.value);
134+
e.ports[0].postMessage({ type: "code" });
120135
}
121136
}
122137
}
123138

124139
const worker = new JavascriptTestEvaluator();
125140

126-
onmessage = function (e: TestEvent | InitEvent<InitWorkerOptions>) {
141+
onmessage = function (e: TestEvent | InitEvent<InitWorkerOptions> | CodeEvent) {
127142
void worker.handleMessage(e);
128143
};

packages/main/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export class FCCTestRunner {
5050
beforeAll?: string;
5151
beforeEach?: string;
5252
afterEach?: string;
53+
afterAll?: string;
5354
};
5455
loadEnzyme?: boolean;
5556
}) {

packages/main/src/test-runner.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
InitTestFrameOptions,
77
Pass,
88
Fail,
9+
CodeEvent,
910
} from "../../shared/src/interfaces/test-evaluator";
1011

1112
import {
@@ -70,6 +71,7 @@ const hideFrame = (iframe: HTMLIFrameElement) => {
7071
export class DOMTestRunner implements Runner {
7172
#testEvaluator: HTMLIFrameElement;
7273
#script: string;
74+
#afterAll?: string;
7375

7476
#createTestEvaluator({ assetPath, script }: RunnerConfig) {
7577
const iframe = document.createElement("iframe");
@@ -99,6 +101,8 @@ ${hooks.beforeAll}
99101
</script>`
100102
: "";
101103

104+
this.#afterAll = hooks?.afterAll;
105+
102106
const isReady = new Promise((resolve) => {
103107
const listener = () => {
104108
this.#testEvaluator.removeEventListener("load", listener);
@@ -155,6 +159,18 @@ ${opts.source}`;
155159
return result;
156160
}
157161

162+
async #runCode(code: string) {
163+
const msg: CodeEvent["data"] = {
164+
type: "code",
165+
value: code,
166+
};
167+
168+
return post({
169+
messenger: this.#testEvaluator.contentWindow!,
170+
message: msg,
171+
});
172+
}
173+
158174
async runAllTests(tests: string[]): Promise<(Pass | Fail)[]> {
159175
const results: (Pass | Fail)[] = [];
160176

@@ -164,6 +180,10 @@ ${opts.source}`;
164180
results.push(result);
165181
}
166182

183+
if (this.#afterAll) {
184+
await this.#runCode(this.#afterAll);
185+
}
186+
167187
return results;
168188
}
169189

@@ -216,6 +236,18 @@ export class WorkerTestRunner implements Runner {
216236
}
217237
}
218238

239+
async #runCode(code: string) {
240+
const msg: CodeEvent["data"] = {
241+
type: "code",
242+
value: code,
243+
};
244+
245+
return post({
246+
messenger: this.#testEvaluator,
247+
message: msg,
248+
});
249+
}
250+
219251
async runTest(test: string, timeout = 5000) {
220252
let terminateTimeoutId: ReturnType<typeof setTimeout> | undefined;
221253
const terminate = new Promise<Fail>((resolve) => {
@@ -253,6 +285,10 @@ export class WorkerTestRunner implements Runner {
253285
results.push(result);
254286
}
255287

288+
if (this.#opts?.hooks?.afterAll) {
289+
await this.#runCode(this.#opts.hooks.afterAll);
290+
}
291+
256292
return results;
257293
}
258294

packages/python-evaluator/src/python-test-evaluator.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Pass,
1414
TestEvaluator,
1515
TestEvent,
16+
CodeEvent,
1617
} from "../../shared/src/interfaces/test-evaluator";
1718
import { ReadyEvent } from "../../shared/src/interfaces/test-runner";
1819
import { postCloneableMessage } from "../../shared/src/messages";
@@ -216,8 +217,18 @@ class PythonTestEvaluator implements TestEvaluator {
216217
return this.#runTest!(test);
217218
}
218219

220+
async runCode(code: string) {
221+
try {
222+
await eval(code);
223+
} catch (err) {
224+
// If the code throws an error, we want to log it to the console
225+
// so that it can be debugged.
226+
console.error("Error evaluating code:", code, err);
227+
}
228+
}
229+
219230
async handleMessage(
220-
e: TestEvent | InitEvent<InitWorkerOptions>,
231+
e: TestEvent | InitEvent<InitWorkerOptions> | CodeEvent,
221232
): Promise<void> {
222233
if (e.data.type === "test") {
223234
const result = await this.#runTest!(e.data.value);
@@ -226,12 +237,18 @@ class PythonTestEvaluator implements TestEvaluator {
226237
} else if (e.data.type === "init") {
227238
await this.init(e.data.value);
228239
postMessage(READY_MESSAGE);
240+
} else if (e.data.type === "code") {
241+
// This is used to run arbitrary non-test code, such as the afterAll hook.
242+
await this.runCode(e.data.value);
243+
e.ports[0].postMessage({ type: "code" });
229244
}
230245
}
231246
}
232247

233248
const worker = new PythonTestEvaluator();
234249

235-
globalThis.onmessage = function (e: TestEvent | InitEvent<InitWorkerOptions>) {
250+
globalThis.onmessage = function (
251+
e: TestEvent | InitEvent<InitWorkerOptions> | CodeEvent,
252+
) {
236253
void worker.handleMessage(e);
237254
};

packages/shared/src/interfaces/test-evaluator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface Fail extends Logged {
2020
}
2121

2222
export type TestEvent = MessageEvent<{ type: "test"; value: string }>;
23+
export type CodeEvent = MessageEvent<{ type: "code"; value: string }>;
2324
export type InitEvent<Data> = MessageEvent<{
2425
type: "init";
2526
value: Data;
@@ -36,6 +37,7 @@ export interface InitTestFrameOptions {
3637
beforeAll?: string;
3738
beforeEach?: string;
3839
afterEach?: string;
40+
afterAll?: string;
3941
};
4042
}
4143

@@ -49,6 +51,7 @@ export interface InitWorkerOptions {
4951
beforeEach?: string;
5052
beforeAll?: string;
5153
afterEach?: string;
54+
afterAll?: string;
5255
};
5356
}
5457

packages/tests/integration-tests/index.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ for(let i = 0; i < 3; i++) {
416416
]);
417417
});
418418

419-
it("should handle empty array in runAllTests", async () => {
419+
it("should be able to handle empty arrays passed to runAllTests", async () => {
420420
const result = await page.evaluate(async (type) => {
421421
const runner = await window.FCCTestRunner.createTestRunner({
422422
type,
@@ -428,6 +428,72 @@ for(let i = 0; i < 3; i++) {
428428
expect(result).toEqual([]);
429429
});
430430

431+
it("should run the afterAll hook after running all tests", async () => {
432+
const result = await page.evaluate(async (type) => {
433+
const runner = await window.FCCTestRunner.createTestRunner({
434+
type,
435+
hooks: {
436+
beforeAll: "globalThis.x = 0;",
437+
afterAll: "globalThis.x = 1;",
438+
},
439+
});
440+
441+
// Individual tests do not trigger the afterAll hook, but runAllTests
442+
// does.
443+
const initial = await runner.runAllTests(["assert.equal(x, 0);"]);
444+
445+
// Check that afterAll ran
446+
const after = await runner.runTest("assert.equal(x, 1);");
447+
448+
return [initial, after];
449+
}, type);
450+
451+
expect(result).toEqual([[{ pass: true }], { pass: true }]);
452+
});
453+
454+
it("should run the afterAll hook even if a test fails", async () => {
455+
const result = await page.evaluate(async (type) => {
456+
const runner = await window.FCCTestRunner.createTestRunner({
457+
type,
458+
hooks: {
459+
beforeAll: "globalThis.x = 0;",
460+
afterAll: "globalThis.x = 1;",
461+
},
462+
});
463+
464+
// Run a failing test
465+
await runner.runAllTests([
466+
"assert.equal(x, 0);",
467+
"assert.equal(1, 2);", // This should fail
468+
]);
469+
470+
// Check that afterAll ran
471+
return runner.runTest("assert.equal(x, 1);");
472+
}, type);
473+
474+
expect(result).toEqual({ pass: true });
475+
});
476+
477+
it("should console.error the error if the afterAll hook fails", async () => {
478+
expect.assertions(2);
479+
page.once("console", (msg) => {
480+
expect(msg.type()).toBe("error");
481+
});
482+
483+
const result = await page.evaluate(async (type) => {
484+
const runner = await window.FCCTestRunner.createTestRunner({
485+
type,
486+
hooks: {
487+
afterAll: "throw new Error('afterAll error');",
488+
},
489+
});
490+
491+
return runner.runAllTests(["assert.equal(1, 1);"]);
492+
}, type);
493+
494+
expect(result).toEqual([{ pass: true }]);
495+
});
496+
431497
it("should support top-level await in tests", async () => {
432498
const result = await page.evaluate(async (type) => {
433499
const runner = await window.FCCTestRunner.createTestRunner({

0 commit comments

Comments
 (0)