Skip to content

Commit c38e051

Browse files
feat: support top-level await in tests (#385)
1 parent 996e870 commit c38e051

File tree

5 files changed

+78
-14
lines changed

5 files changed

+78
-14
lines changed

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

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { MockLocalStorage } from "./mock-local-storage";
2323
import { ProxyConsole } from "../../shared/src/proxy-console";
2424
import { format } from "../../shared/src/format";
25+
import { createAsyncIife } from "../../shared/src/async-iife";
2526

2627
const READY_MESSAGE: ReadyEvent["data"] = { type: "ready" };
2728

@@ -112,15 +113,30 @@ export class DOMTestEvaluator implements TestEvaluator {
112113
Enzyme.configure({ adapter: new Adapter16() });
113114
}
114115

115-
this.#runTest = async function (testString: string): Promise<Fail | Pass> {
116+
this.#runTest = async function (rawTest: string): Promise<Fail | Pass> {
116117
this.#proxyConsole.on();
117118
try {
118-
// Eval test string to actual JavaScript
119-
// This return can be a function
120-
// i.e. function() { assert(true, 'happy coding'); }
121-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
122-
const test = await eval(`${opts.hooks?.beforeEach ?? ""}
123-
${testString}`);
119+
let test;
120+
try {
121+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
122+
test = await eval(`${opts.hooks?.beforeEach ?? ""}
123+
${rawTest}`);
124+
} catch (err) {
125+
if (
126+
err instanceof SyntaxError &&
127+
err.message.includes(
128+
"await is only valid in async functions and the top level bodies of modules",
129+
)
130+
) {
131+
const iifeTest = createAsyncIife(rawTest);
132+
// There's no need to assign this to 'test', since it replaces that
133+
// functionality.
134+
await eval(iifeTest);
135+
} else {
136+
throw err;
137+
}
138+
}
139+
124140
if (typeof test === "function") {
125141
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
126142
await test();

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
import type { ReadyEvent } from "../../shared/src/interfaces/test-runner";
1414
import { postCloneableMessage } from "../../shared/src/messages";
1515
import { format } from "../../shared/src/format";
16+
import { createAsyncIife } from "../../shared/src/async-iife";
1617
import { ProxyConsole } from "../../shared/src/proxy-console";
1718

1819
const READY_MESSAGE: ReadyEvent["data"] = { type: "ready" };
@@ -28,11 +29,6 @@ globalThis.__helpers = curriculumHelpers;
2829
Object.freeze(globalThis.__helpers);
2930
Object.freeze(globalThis.assert);
3031

31-
// The newline is important, because otherwise comments will cause the trailing
32-
// `}` to be ignored, breaking the tests.
33-
const wrapCode = (code: string) => `(async () => {${code};
34-
})();`;
35-
3632
// TODO: currently this is almost identical to DOMTestEvaluator, can we make
3733
// it more DRY? Don't attempt until they're both more fleshed out.
3834
export class JavascriptTestEvaluator implements TestEvaluator {
@@ -50,7 +46,7 @@ export class JavascriptTestEvaluator implements TestEvaluator {
5046

5147
this.#runTest = async (rawTest) => {
5248
this.#proxyConsole.on();
53-
const test = wrapCode(rawTest);
49+
const test = createAsyncIife(rawTest);
5450
// This can be reassigned by the eval inside the try block, so it should be declared as a let
5551
// eslint-disable-next-line prefer-const
5652
let __userCodeWasExecuted = false;

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ReadyEvent } from "../../shared/src/interfaces/test-runner";
1818
import { postCloneableMessage } from "../../shared/src/messages";
1919
import { format } from "../../shared/src/format";
2020
import { ProxyConsole } from "../../shared/src/proxy-console";
21+
import { createAsyncIife } from "../../shared/src/async-iife";
2122

2223
type EvaluatedTeststring = {
2324
input?: string[];
@@ -95,7 +96,19 @@ class PythonTestEvaluator implements TestEvaluator {
9596
const test: unknown = eval(testString);
9697
resolve(test);
9798
} catch (err) {
98-
reject(err as Error);
99+
if (
100+
err instanceof SyntaxError &&
101+
err.message.includes(
102+
"await is only valid in async functions and the top level bodies of modules",
103+
)
104+
) {
105+
const iifeTest = createAsyncIife(testString);
106+
107+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
108+
eval(iifeTest).then(resolve).catch(reject);
109+
} else {
110+
reject(err as Error);
111+
}
99112
}
100113
},
101114
);

packages/shared/src/async-iife.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// The newline is important, because otherwise comments will cause the trailing
2+
// `}` to be ignored, breaking the tests.
3+
export const createAsyncIife = (code: string) => `(async () => {${code};
4+
})();`;

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,41 @@ for(let i = 0; i < 3; i++) {
427427

428428
expect(result).toEqual([]);
429429
});
430+
431+
it("should support top-level await in tests", async () => {
432+
const result = await page.evaluate(async (type) => {
433+
const runner = await window.FCCTestRunner.createTestRunner({
434+
type,
435+
});
436+
437+
return runner.runTest(`
438+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
439+
await delay(10);
440+
assert.equal(1, 2);`);
441+
}, type);
442+
expect(result).toMatchObject({ err: { actual: 1 } });
443+
});
444+
445+
// The old api wasn't consistent: only DOM tests supported async tests, but
446+
// this needs to be maintained while it's deprecated.
447+
it.runIf(type === "dom")(
448+
"should support `async () => {}` style tests",
449+
async () => {
450+
const result = await page.evaluate(async (type) => {
451+
const runner = await window.FCCTestRunner.createTestRunner({
452+
type,
453+
});
454+
455+
return runner.runTest(`
456+
async () => {
457+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
458+
await delay(10);
459+
assert.equal(1, 2);
460+
}`);
461+
}, type);
462+
expect(result).toMatchObject({ err: { actual: 1 } });
463+
},
464+
);
430465
});
431466

432467
describe.each([

0 commit comments

Comments
 (0)