import { ArrayPrototypePush, RangeError, TypeError, } from "ext:core/primordials"; import { flag, optionsOf, registry, type Definition } from "./shared.js"; import { required, usv } from "ext:web/idl.js"; // The runner captures intrinsics before guest evaluation can poison globals. import "./runner.js"; const DEFAULT_TIMEOUT_MS = 5000; /** Context passed to each test function; also the type of nested step contexts. */ export interface TestContext { readonly name: string; /** * Runs a substep sequentially and reports it indented under its parent. * Resolves true when the step passed, false when it was ignored or * failed; a failing step never throws, but any failed step still fails * its ancestors up to the test. Steps share their test's timeout; * `only` and `timeout` options are rejected. Only top-level tests count * toward file totals. */ step(name: string, fn: (t: TestContext) => void | Promise): Promise; step(options: { name: string; fn?: (t: TestContext) => void | Promise; ignore?: boolean; }): Promise; } export interface TestOptions { name: string; /** Required unless `ignore` is true. */ fn?: (t: TestContext) => void | Promise; ignore?: boolean; /** When any test in a file sets this, the rest of the file is filtered out. */ only?: boolean; /** * Milliseconds before the test fails; defaults to 5000, 0 requires the * body to settle synchronously, past 2147483647 is rejected, and * Infinity disables the per-test timeout (the file-level 60s progress * budget still applies). Covers the test body and all of its steps * together, including steps never awaited. */ timeout?: number; } function checkTimeout(value: unknown): number { if (typeof value !== "number" || value !== value) { throw new TypeError("Test timeout must be a number of milliseconds"); } if (value < 0) { throw new RangeError("Test timeout must be zero or more milliseconds"); } // Timers truncate to 32 bits, so larger finite values would fire immediately. if (value !== Infinity && value > 2147483647) { throw new RangeError( "Test timeout must be at most 2147483647ms; use Infinity for no timeout", ); } return value; } function parseTest(nameOrOptions: unknown, fn: unknown, only: boolean, ignore: boolean, count: number): Definition { required(count, 1); let name, func = fn, timeout = DEFAULT_TIMEOUT_MS; if (typeof nameOrOptions === "string") { // An undefined function falls through to the missing-function check so // that test.ignore('name') reads naturally. if (count > 2 || (count === 2 && fn !== undefined && typeof fn !== "function")) { throw new TypeError("Expected test(name, fn)"); } name = usv(nameOrOptions); } else { if (count > 1) throw new TypeError("Expected test(options)"); const options = optionsOf(nameOrOptions, ["name", "fn", "ignore", "only", "timeout"], "test"); if (options.name === undefined) throw new TypeError("Test requires a name"); name = usv(options.name); func = options.fn; ignore = flag(options, "ignore", "Test") || ignore; only = flag(options, "only", "Test") || only; if (options.timeout !== undefined) timeout = checkTimeout(options.timeout); } if (func === undefined) { if (!ignore) throw new TypeError(`Test "${name}" requires a function`); } else if (typeof func !== "function") { throw new TypeError(`Test "${name}" requires a function`); } return { name, fn: func, ignore, only, timeout } as Definition; } function push(nameOrOptions: unknown, fn: unknown, only: boolean, ignore: boolean, count: number): void { // Registrations after collection would silently never run. if (registry.sealed) throw new TypeError("test() must be called while the test file evaluates"); ArrayPrototypePush(registry.definitions, parseTest(nameOrOptions, fn, only, ignore, count)); } const registration = function test(nameOrOptions: unknown, fn?: unknown): void { push(nameOrOptions, fn, false, false, arguments.length); }; registration.only = function only(nameOrOptions: unknown, fn?: unknown): void { push(nameOrOptions, fn, true, false, arguments.length); }; registration.ignore = function ignore(nameOrOptions: unknown, fn?: unknown): void { push(nameOrOptions, fn, false, true, arguments.length); }; /** * Registers a test in the current file. Only `avril test` runs registered * tests; under `run` and `serve` registration is inert. Each file runs in * its own fresh isolate, tests within a file run sequentially in * registration order, and a `pledge()` inside one test narrows every test * after it in the same file. There are no resource or timer sanitizers. */ export const test: ( ((name: string, fn: (t: TestContext) => void | Promise) => void) & ((options: TestOptions) => void) ) & { /** Registers a test that runs only when some test in its file uses `only`. */ only: ((name: string, fn: (t: TestContext) => void | Promise) => void) & ((options: TestOptions) => void); /** Registers a test that is collected but never runs. */ ignore: ((name: string, fn?: (t: TestContext) => void | Promise) => void) & ((options: TestOptions) => void); } = registration;