// Derived from Node.js v24.16.0 (MIT; Copyright Node.js contributors, and Joyent, Inc. and // other Node contributors). See LICENSE-node. // node:internal/test_runner/test.js: Test, Suite, TestHook, TestContext, SuiteContext. // Trimmed from Node's file: no coverage, no randomize/seed, no rerun-failures replay, // no source-mapped locations, no diagnostics_channel publishing, no watch mode. // testNamePatterns/testSkipPatterns filtering (willBeFilteredByName, // getTestNameWithAncestors, testMatchesPattern) IS ported, reading harness.config — // but nothing feeds that config today: run()'s own testNamePatterns/testSkipPatterns // option never reaches it for isolation:'none' (confirmed against real Node 24.16.0 — // see runner.ts), and no CLI flag parser exists here (DESIGN: refused) to set it the // other way. It is wired so a later CLI parser only has to feed commandLineDefaults(). The // `only`-filtering behaviour direct execution actually exercises (confirmed against // real Node 24.16.0: `node file.js` always filters by `only`, unlike `--test`, because // harness.js's `isFilteringByOnly` is hardcoded true outside `--test`/run()) is kept. // Concurrency is accepted and validated like Node's, and subtests under `concurrency: // true` do interleave (their async bodies race on this single-threaded worker's // cooperative loop the same way Node's real parallel scheduling only ever interleaves // microtask-level work, not CPU-bound synchronous code) — confirmed against real Node // 24.16.0 with a `describe(..., { concurrency: true })` of two subtests that each log // before/after an awaited delay: both goldens print `a1,b1,b2,a2`. import { invalidArgType, invalidArgValue, outOfRange } from "../errors"; import { assert as nodeAssert } from "../assert"; import { testFailure, type TestFailureError } from "./test-failure"; import { TestsStream, type Loc } from "./tests_stream"; import { getAssertionMap } from "./assert"; import { isTestFailureError, countCompletedTest, createDeferredCallback, getCallerLocation } from "./utils"; import { createMockTracker, MockTracker } from "./mock/mock"; import { SnapshotManager } from "./snapshot"; export const kCancelledByParent = "cancelledByParent"; const kAborted = "testAborted"; const kParentAlreadyFinished = "parentAlreadyFinished"; export const kSubtestsFailed = "subtestsFailed"; const kTestCodeFailure = "testCodeFailure"; const kTestTimeoutFailure = "testTimeoutFailure"; const kExpectedFailure = "expectedFailure"; const kHookFailure = "hookFailed"; const kCallbackAndPromisePresent = "callbackAndPromisePresent"; const kDefaultTimeout: number | null = null; export const kUnwrapErrors = new Set([kTestCodeFailure, kHookFailure, "uncaughtException", "unhandledRejection"]); const kHookNames = ["before", "after", "beforeEach", "afterEach"] as const; type HookName = (typeof kHookNames)[number]; const noop = (): void => {}; // Node's lib/internal/timers.js TIMEOUT_MAX: the largest delay setTimeout/setInterval // accept (a signed 32-bit int), reused here as the upper bound for timeout/plan.wait/ // waitFor's interval and timeout the way test.js's validateNumber calls do. const TIMEOUT_MAX = 2 ** 31 - 1; const MAX_UINT32 = 4_294_967_295; // No shared validateUint32/validateNumber/validateAbortSignal exists in src/runtime (each // module that needs Node's internal/validators.js checks keeps its own small copy — see // e.g. histogram.ts's validateInteger, readline/promises.ts's validateInteger); these // mirror internal/validators.js's validateUint32, validateNumber and validateAbortSignal // for the handful of test_runner options that need them. function validateUint32(value: unknown, name: string, positive = false): void { if (typeof value !== "number") throw invalidArgType(name, "number", value); if (!Number.isInteger(value)) throw outOfRange(name, "an integer", value); const min = positive ? 1 : 0; if (value < min || value > MAX_UINT32) throw outOfRange(name, `>= ${min} && <= ${MAX_UINT32}`, value); } function validateNumber(value: unknown, name: string, min?: number, max?: number): void { if (typeof value !== "number") throw invalidArgType(name, "number", value); if ((min != null && value < min) || (max != null && value > max) || ((min != null || max != null) && Number.isNaN(value))) { const range = `${min != null ? `>= ${min}` : ""}${min != null && max != null ? " && " : ""}${max != null ? `<= ${max}` : ""}`; throw outOfRange(name, range, value); } } function validateAbortSignal(signal: unknown, name: string): void { if (signal !== undefined && (signal === null || typeof signal !== "object" || !("aborted" in signal))) { throw invalidArgType(name, "an instance of AbortSignal", signal); } } /** Node's test.js testMatchesPattern: a pattern matches by the test's own name, by any * ancestor's name (a matched suite drags its subtests along), or by the full * "grandparent parent test" ancestor chain (so a non-unique leaf name can still be * pinned down). `.exec()`, not `.test()`, to mirror Node's RegExpPrototypeExec exactly * — including its `lastIndex` statefulness for a pattern carrying the `g`/`y` flag. */ function testMatchesPattern(test: Test, patterns: RegExp[]): boolean { const matchesByNameOrParent = patterns.some((re) => re.exec(test.name) !== null) || (test.parent !== null && testMatchesPattern(test.parent, patterns)); if (matchesByNameOrParent) return true; const testNameWithAncestors = test.getTestNameWithAncestors().trim(); return patterns.some((re) => re.exec(testNameWithAncestors) !== null); } /** The test whose body (fn, or before/beforeEach/afterEach/after hook) is currently * executing. The harness's uncaughtException/unhandledRejection handlers attribute an * async error to this test (or the root test when nothing is running), the closest * honest equivalent of Node's async_hooks-based resource-to-test attribution * (async_hooks.createHook is refused here — see divergences.json "test." * and DESIGN.md's refusals section). */ let currentlyRunning: Test | null = null; export function getCurrentlyRunningTest(): Test | null { return currentlyRunning; } /** The nearest Suite whose builder callback is on the stack right now, pushed and * popped around the synchronous part of a Suite's body (Suite#createBuild). * harness.ts's top-level test()/describe()/hooks read it, falling back to the root. * Node answers this through async_hooks (testResources keyed by asyncId), which * follows awaits. The kernel has no async context, so a test()/it() registered after * an await inside an async describe() builder lands under the root here, not under * the suite. */ const buildingStack: Test[] = []; export function pushBuildingParent(parent: Test): void { buildingStack.push(parent); } export function popBuildingParent(): void { buildingStack.pop(); } export function currentBuildingParent(): Test | null { return buildingStack[buildingStack.length - 1] ?? null; } export interface Harness { config: { timeout: number; only: boolean; cwd: string; updateSnapshots: boolean; testNamePatterns: RegExp[] | null; testSkipPatterns: RegExp[] | null; }; counters: { tests: number; failed: number; passed: number; cancelled: number; skipped: number; todo: number; topLevel: number; suites: number; }; success: boolean; isFilteringByOnly: boolean; /** True when either pattern list is non-null — runner.ts's commandLineDefaults() * path (a later CLI parser would set this the same way). Read once per subtest, in * the constructor, to decide whether to call willBeFilteredByName() at all. */ isFilteringByName: boolean; reporter?: TestsStream; reported?: boolean; snapshotManager?: SnapshotManager; /** Node's harness.bootstrapPromise: null once resolved, otherwise every top-level * test()/describe() call (harness.ts's startSubtestAfterBootstrap) awaits it before * actually starting. createTestTree() seeds it with an already-resolved promise * (Node's runBootstrap(), which only does real async work for options.globalSetupPath * — unwired here, see runner.ts's header); run()'s isolation:'none' path (runner.ts's * runIsolatedNone) replaces it with one that stays pending until every test file has * loaded, which is the entire race fix: no top-level test can start — let alone * finish and trigger finalize()'s "every top-level test is done" check below — before * every file has had a chance to register its own. */ bootstrapPromise?: Promise | null; /** Node's harness.buildPromise: resolves once every *top-level* Suite's buildSuite * (and, transitively, everything synchronously reachable from it) has settled — * harness.ts's startSubtestAfterBootstrap awaits it, nulling it once, before any * top-level test()/describe() actually starts(); Test#start() (below) re-checks it * so a nested subtest started from inside a Suite.run() that happens to race ahead * of the gate also defers. Seeded with a pending promise in createTestTree(); * resolved by waitForBuildPhase() once harness.buildSuites settles. This is the * fix for exactly the bug this file's history records: without it, a describe() * whose builder callback awaits (an async describe) lets sibling top-level tests * that build synchronously start and finish first, so the spec reporter sees them * — and reports them — before the async describe's own subtests ever got attached. */ buildPromise?: Promise | null; /** Node's harness.buildSuites: every top-level Suite's `buildSuite` promise, * collected by startSubtestAfterBootstrap as each top-level describe() registers, * then awaited together (Promise.all) by waitForBuildPhase() before buildPromise * resolves. */ buildSuites?: Promise[]; /** Node's harness.isWaitingForBuildPhase: set once a microtask has been queued to * call waitForBuildPhase(), so the second (and later) top-level registration in the * same synchronous turn doesn't queue a redundant one. */ isWaitingForBuildPhase?: boolean; /** Node's harness.waitForBuildPhase(): awaits every buildSuites entry collected so * far, then resolves buildPromise. Queued as a microtask by the first top-level * registration, so by the time it runs, every top-level test()/describe() call made * synchronously in this turn has already pushed its own buildSuite into the array. */ waitForBuildPhase?: () => Promise; } export interface TestOptions { name?: string; fn?: TestFn; parent?: Test | null; concurrency?: number | boolean; only?: boolean; timeout?: number; todo?: boolean | string; skip?: boolean | string; signal?: AbortSignal; plan?: number; expectFailure?: ExpectFailure; loc?: [number, number, string]; harness?: Harness; entryFile?: string; hookType?: HookName; } export type ExpectFailure = boolean | string | RegExp | ((err: unknown) => boolean) | { label?: string; match?: RegExp | ((err: unknown) => boolean) }; type ParsedExpectFailure = false | { label: string | undefined; match: RegExp | ((err: unknown) => boolean) | undefined }; function parseExpectFailure(expectFailure: ExpectFailure | undefined): ParsedExpectFailure { if (expectFailure === undefined || expectFailure === false) return false; if (typeof expectFailure === "string") return { label: expectFailure, match: undefined }; if (typeof expectFailure === "function" || expectFailure instanceof RegExp) return { label: undefined, match: expectFailure }; if (typeof expectFailure !== "object") return { label: undefined, match: undefined }; const keys = Object.keys(expectFailure); if (keys.length === 0) throw invalidArgValue("options.expectFailure", expectFailure, "must not be an empty object"); if (keys.every((k) => k === "match" || k === "label")) return { label: expectFailure.label, match: expectFailure.match }; return { label: undefined, match: undefined }; } export type TestFn = (ctx: TestContext | SuiteContext, cb?: (err?: unknown) => void) => unknown; export interface RunArgs { ctx: TestContext | SuiteContext; args: [TestContext] | [SuiteContext]; } export class TestPlan { expected: number; actual = 0; wait: boolean | number | undefined; #waitIndefinitely = false; #planPromise: { promise: Promise; resolve: () => void; reject: (err: unknown) => void } | null = null; #timeoutId: ReturnType | null = null; constructor(count: number, options: { wait?: boolean | number } = {}) { validateUint32(count, "count"); if (options === null || typeof options !== "object") throw invalidArgType("options", "Object", options); this.expected = count; const { wait } = options; if (typeof wait === "boolean") { this.wait = wait; this.#waitIndefinitely = wait; } else if (typeof wait === "number") { validateNumber(wait, "options.wait", 0, TIMEOUT_MAX); this.wait = wait; } else if (wait !== undefined) { throw invalidArgType("options.wait", "one of type boolean or number", wait); } } #planMet(): boolean { return this.actual === this.expected; } #shouldWait(): boolean { return this.wait !== undefined && this.wait !== false; } check(): Promise | undefined { if (this.#planMet()) { if (this.#timeoutId !== null) { clearTimeout(this.#timeoutId); this.#timeoutId = null; } if (this.#planPromise) { this.#planPromise.resolve(); this.#planPromise = null; } return; } if (!this.#shouldWait()) { throw testFailure(`plan expected ${this.expected} assertions but received ${this.actual}`, kTestCodeFailure); } if (!this.#planPromise) { let resolve!: () => void; let reject!: (err: unknown) => void; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); this.#planPromise = { promise, resolve, reject }; if (!this.#waitIndefinitely) { this.#timeoutId = setTimeout(() => { reject(testFailure( `plan timed out after ${this.wait}ms with ${this.actual} assertions when expecting ${this.expected}`, kTestTimeoutFailure, )); }, this.wait as number); } } return this.#planPromise.promise; } count(): void { this.actual++; if (this.#planPromise) this.check(); } } function getFullName(test: Test): string { if (test === test.root) return test.name; let fullName = test.name; for (let t = test.parent!; t !== t.root; t = t.parent!) fullName = `${t.name} > ${fullName}`; return fullName; } function stopTest(timeout: number | null, signal: AbortSignal): { promise: Promise; dispose(): void } { let resolve!: () => void; const base = new Promise((res) => (resolve = res)); const onAbort = (): void => resolve(); signal.addEventListener("abort", onAbort); let timer: ReturnType | undefined; let promise = base; let dispose: () => void; if (timeout === kDefaultTimeout) { dispose = () => signal.removeEventListener("abort", onAbort); } else { timer = setTimeout(resolve, timeout!); (timer as unknown as { unref?: () => void }).unref?.(); promise = base.then(() => { throw testFailure(`test timed out after ${timeout}ms`, kTestTimeoutFailure); }); dispose = () => { signal.removeEventListener("abort", onAbort); clearTimeout(timer); }; } return { promise, dispose }; } export class TestContext { #assert: Record unknown> | undefined; #test: Test; constructor(test: Test) { this.#test = test; } get signal(): AbortSignal { return this.#test.signal; } get name(): string { return this.#test.name; } get filePath(): string | undefined { return this.#test.entryFile; } get fullName(): string { return getFullName(this.#test); } get error(): unknown { return this.#test.error; } get passed(): boolean { return this.#test.passed; } get attempt(): number { return 0; } get workerId(): number | undefined { return undefined; } diagnostic(message: string): void { this.#test.diagnostic(message); } plan(count: number, options: { wait?: boolean | number } = {}): void { if (this.#test.plan !== null) throw testFailure("cannot set plan more than once", kTestCodeFailure); this.#test.plan = new TestPlan(count, options); } get assert(): Record unknown> { if (this.#assert === undefined) { const test = this.#test; const map = getAssertionMap(); // Node's lazyAssertObject(harness): the shared assertion map gains 'snapshot'/ // 'fileSnapshot' once, ever, for the process — the SnapshotManager instance is // created from whichever harness happens to reach here first (there is only // ever one harness per worker here, so this always matches Node). if (!map.has("snapshot")) { // A subtest's own `.harness` field is null (only the root Test carries it — // see Test's constructor/`harness` field above); every test reaches the same // harness through `.root.harness`, matching Node's `this.#test.root.harness`. const harness = test.root.harness!; const manager = new SnapshotManager(harness.config.updateSnapshots); harness.snapshotManager = manager; map.set("snapshot", manager.createAssert() as (...args: unknown[]) => unknown); map.set("fileSnapshot", manager.createFileAssert() as (...args: unknown[]) => unknown); } const assertObj: Record unknown> = {}; this.#assert = assertObj; for (const [name, method] of map) { assertObj[name] = (...args: unknown[]) => { if (test.plan !== null) test.plan.count(); return method.apply(this, args); }; } if (!map.has("ok")) { assertObj.ok = (...args: unknown[]) => { if (test.plan !== null) test.plan.count(); nodeAssert.ok(...args); }; } } return this.#assert; } get mock(): unknown { return this.#test.mock(); } runOnly(value: unknown): void { this.#test.runOnlySubtests = Boolean(value); } skip(message?: string): void { this.#test.skip(message); } todo(message?: string): void { this.#test.todo(message); } test(name: unknown, options?: unknown, fn?: unknown): Promise { if (this.#test.plan !== null) this.#test.plan.count(); const subtest = this.#test.createSubtest(Test, name, options, fn, { loc: getCallerLocation() }); return subtest.start(); } before(fn: unknown, options?: unknown): void { this.#test.createHook("before", fn, { ...(options as object), parent: this.#test, hookType: "before", loc: getCallerLocation() }); } after(fn: unknown, options?: unknown): void { this.#test.createHook("after", fn, { ...(options as object), parent: this.#test, hookType: "after", loc: getCallerLocation() }); } beforeEach(fn: unknown, options?: unknown): void { this.#test.createHook("beforeEach", fn, { ...(options as object), parent: this.#test, hookType: "beforeEach", loc: getCallerLocation() }); } afterEach(fn: unknown, options?: unknown): void { this.#test.createHook("afterEach", fn, { ...(options as object), parent: this.#test, hookType: "afterEach", loc: getCallerLocation() }); } /** Node's TestContext#waitFor: poll `condition` every `interval`ms (default 50) until * it returns/resolves truthy-or-whatever, or `timeout`ms (default 1000) elapses. */ waitFor(condition: unknown, options: { interval?: number; timeout?: number } = {}): Promise { if (typeof condition !== "function") throw invalidArgType("condition", "of type function", condition); if (options === null || typeof options !== "object") throw invalidArgType("options", "Object", options); const { interval = 50, timeout = 1000 } = options; validateNumber(interval, "options.interval", 0, TIMEOUT_MAX); validateNumber(timeout, "options.timeout", 0, TIMEOUT_MAX); let resolve!: (v: unknown) => void; let reject!: (e: unknown) => void; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); let cause: unknown; const noError = Symbol(); let causeSet = false; let pollerId: ReturnType | undefined; const timeoutId = setTimeout(() => { clearTimeout(pollerId); const err = new Error("waitFor() timed out"); if (causeSet) (err as Error & { cause?: unknown }).cause = cause; reject(err); }, timeout); const poller = async (): Promise => { try { const result = await (condition as () => unknown)(); clearTimeout(timeoutId); resolve(result); } catch (err) { cause = err; causeSet = true; pollerId = setTimeout(poller, interval); } }; void poller(); void noError; return promise; } } export class SuiteContext { #suite: Suite; constructor(suite: Suite) { this.#suite = suite; } get signal(): AbortSignal { return this.#suite.signal; } get name(): string { return this.#suite.name; } get filePath(): string | undefined { return this.#suite.entryFile; } get fullName(): string { return getFullName(this.#suite); } get passed(): boolean { return this.#suite.passed; } get attempt(): number { return 0; } diagnostic(message: string): void { this.#suite.diagnostic(message); } } interface PendingSubtest { test: Test; resolve: () => void; } export class Test { reportedType: "test" | "suite" = "test"; abortController = new AbortController(); outerSignal: AbortSignal | undefined; signal: AbortSignal; name: string; parent: Test | null; root: Test; harness: Harness | null = null; config: Harness["config"]; concurrency = 1; nesting: number; only: boolean | undefined; reporter: TestsStream; runOnlySubtests: boolean; childNumber: number; timeout: number | null; entryFile: string | undefined; testId: number; loc: Loc | undefined; fn: TestFn; mockTracker: MockTracker | null = null; plan: TestPlan | null = null; expectedAssertions: number | undefined; cancelled = false; expectFailure: ParsedExpectFailure; skipped: boolean; isTodo: boolean; startTime: number | null = null; endTime: number | null = null; passed = false; error: unknown = null; message: string | null; activeSubtests = 0; pendingSubtests: PendingSubtest[] = []; pendingSubtestsScheduled = false; readySubtests = new Map(); unfinishedSubtests = new Set(); subtestsPromise: { promise: Promise; resolve: () => void } | null = null; subtests: Test[] = []; nextReportOrder = 1; reportOrder = 0; waitingOn = 0; finished = false; filtered = false; filteredByName = false; hasOnlyTests = false; testNumber = 0; outputSubtestCount = 0; diagnostics: string[] = []; nextTestId = 1; hookType: HookName | undefined; reported = false; #reportedSubtest = false; hooks: { before: TestHook[]; after: TestHook[]; beforeEach: TestHook[]; afterEach: TestHook[]; ownAfterEachCount: number } = { before: [], after: [], beforeEach: [], afterEach: [], ownAfterEachCount: 0, }; /** Always false on a plain Test; Suite's own field (below) flips to true once its * createBuild() settles. Declared here (not just on Suite) so createSubtest's guard * can read it on any parent without an instanceof check, matching Node's base * Test#buildPhaseFinished, which is likewise undefined (falsy) off a Suite. */ buildPhaseFinished = false; constructor(options: TestOptions) { let { fn, name, parent = null } = options; const { concurrency, entryFile, expectFailure, loc, only, timeout, todo, skip, signal, plan } = options; if (typeof fn !== "function") fn = noop; if (typeof name !== "string" || name === "") name = fn.name || ""; if (!(parent instanceof Test)) parent = null; this.name = name; this.parent = parent; if (parent === null) { this.root = this; this.harness = options.harness!; this.config = this.harness.config; this.concurrency = 1; this.nesting = 0; this.only = this.config.only; this.reporter = new TestsStream(); this.runOnlySubtests = this.only; this.childNumber = 0; this.timeout = kDefaultTimeout; this.entryFile = entryFile; this.testId = 0; } else { const nesting = parent.parent === null ? parent.nesting : parent.nesting + 1; const { config, isFilteringByOnly, isFilteringByName } = parent.root.harness!; this.root = parent.root; this.config = config; this.concurrency = parent.concurrency; this.nesting = nesting; this.only = only; this.reporter = parent.reporter; this.runOnlySubtests = false; this.childNumber = parent.subtests.length + 1; this.timeout = parent.timeout; this.entryFile = parent.entryFile; this.testId = this.root.nextTestId++; if (isFilteringByName) { this.filteredByName = this.willBeFilteredByName(); if (!this.filteredByName) { for (let t: Test | null = this.parent; t !== null && t.filteredByName; t = t.parent) t.filteredByName = false; } } if (isFilteringByOnly) { if (this.only) { this.parent!.runOnlySubtests = true; if (this.parent === this.root || this.parent!.startTime === null) { for (let t: Test | null = this.parent; t !== null && !t.hasOnlyTests; t = t.parent) t.hasOnlyTests = true; } } else if (this.only === false) { fn = noop; } } } switch (typeof concurrency) { case "number": validateUint32(concurrency, "options.concurrency", true); this.concurrency = concurrency; break; case "boolean": this.concurrency = concurrency ? (parent === null ? 1 : Infinity) : 1; break; default: if (concurrency != null) throw invalidArgType("options.concurrency", "one of type boolean or number", concurrency); } if (timeout != null && timeout !== Infinity) { validateNumber(timeout, "options.timeout", 0, TIMEOUT_MAX); this.timeout = timeout; } else if (timeout == null) { const cliTimeout = this.config.timeout; if (cliTimeout != null && cliTimeout !== Infinity) { validateNumber(cliTimeout, "this.config.timeout", 0, TIMEOUT_MAX); this.timeout = cliTimeout; } } if (skip) fn = noop; this.outerSignal = signal; this.signal = this.abortController.signal; validateAbortSignal(signal, "options.signal"); this.outerSignal?.addEventListener("abort", this.#abortHandler); this.fn = fn; this.expectedAssertions = plan; this.expectFailure = parseExpectFailure(expectFailure) || (this.parent?.expectFailure ?? false); this.skipped = skip !== undefined && skip !== false; this.isTodo = (todo !== undefined && todo !== false) || Boolean(this.parent?.isTodo); this.message = typeof skip === "string" ? skip : typeof todo === "string" ? todo : null; this.loc = loc === undefined ? undefined : { line: loc[0], column: loc[1], file: loc[2] }; } applyFilters(): void { if (this.error) return; if (this.filteredByName) { this.filtered = true; return; } if (this.root.harness!.isFilteringByOnly && !this.only && !this.hasOnlyTests) { if (this.parent!.runOnlySubtests || this.parent!.hasOnlyTests || this.only === false) this.filtered = true; } } /** Node's test.js willBeFilteredByName(): read once, from the constructor, against * harness.config.testNamePatterns/testSkipPatterns — null for every path here today * (direct execution, and run(), whose own testNamePatterns/testSkipPatterns option * Node itself never wires into this config for isolation:'none' — see runner.ts's * header comment), so this is dead code until a later CLI flag parser sets either * field through commandLineDefaults(). Kept so that wiring has somewhere to land. */ willBeFilteredByName(): boolean { const { testNamePatterns, testSkipPatterns } = this.config; if (testNamePatterns && !testMatchesPattern(this, testNamePatterns)) return true; if (testSkipPatterns && testMatchesPattern(this, testSkipPatterns)) return true; return false; } /** Node's test.js getTestNameWithAncestors(): "grandparent parent test", used by * testMatchesPattern so a pattern can match a test by its full nested name. */ getTestNameWithAncestors(): string { if (!this.parent) return ""; return `${this.parent.getTestNameWithAncestors()} ${this.name}`; } hasConcurrency(): boolean { return this.concurrency > this.activeSubtests; } addPendingSubtest(p: PendingSubtest): void { this.pendingSubtests.push(p); } schedulePendingSubtests(): void { if (this.pendingSubtestsScheduled) return; this.pendingSubtestsScheduled = true; queueMicrotask(() => { this.pendingSubtestsScheduled = false; void this.processPendingSubtests(); }); } assignReportOrder(subtest: Test): void { if (subtest.reportOrder === 0) subtest.reportOrder = this.nextReportOrder++; } async processPendingSubtests(): Promise { while (this.pendingSubtests.length > 0 && this.hasConcurrency()) { const deferred = this.pendingSubtests.shift()!; const test = deferred.test; this.assignReportOrder(test); test.reporter.dequeue(test.nesting, test.loc, test.name, this.reportedType, test.testId); await test.run(); deferred.resolve(); } } addReadySubtest(subtest: Test): void { this.assignReportOrder(subtest); this.readySubtests.set(subtest.reportOrder, subtest); if (this.unfinishedSubtests.delete(subtest) && this.unfinishedSubtests.size === 0) this.subtestsPromise!.resolve(); } isClearToSend(): boolean { return this.parent === null || (this.parent.waitingOn === this.reportOrder && this.parent.isClearToSend()); } processReadySubtestRange(canSend: boolean): void { const start = this.waitingOn; const end = start + this.readySubtests.size; for (let i = start; i < end; i++) { const subtest = this.readySubtests.get(i); if (subtest === undefined) return; canSend ||= this.isClearToSend(); if (!canSend) return; subtest.finalize(); this.readySubtests.delete(i); } } createSubtest(Factory: typeof Test, nameArg: unknown, optionsArg: unknown, fnArg: unknown, overrides: Partial): Test { let name = nameArg; let options = optionsArg; let fn = fnArg; if (typeof name === "function") { fn = name; name = undefined; } else if (name !== null && typeof name === "object") { fn = options; options = name; name = undefined; } else if (typeof options === "function") { fn = options; options = undefined; } if (options === null || typeof options !== "object") options = {}; let parent: Test = this; // Node's test.js: `this.finished || this.buildPhaseFinished` — a describe() whose // own builder callback already settled (e.g. it threw, or just returned) can still // have a stray test()/describe() call land on it afterward (a callback that fires // late); attach that one to the root instead, same as a call on an already-finished // test. const preventAddingSubtests = this.finished || this.buildPhaseFinished; if (preventAddingSubtests) { while (parent.parent !== null) parent = parent.parent; } const test = new Factory({ fn: fn as TestFn, name: name as string | undefined, parent, ...(options as object), ...overrides }); if (parent.waitingOn === 0) { parent.waitingOn = test.childNumber; let resolve!: () => void; const promise = new Promise((res) => (resolve = res)); parent.subtestsPromise = { promise, resolve }; } if (preventAddingSubtests) { test.fail(testFailure("test could not be started because its parent finished", kParentAlreadyFinished)); } parent.subtests.push(test); return test; } #abortHandler = (): void => { const error = Object.assign(new Error("The test was aborted"), { name: "AbortError", failureType: kAborted }); this.#cancel(error); }; #cancel(error?: unknown): void { if (this.endTime !== null || this.error !== null) return; this.fail(error ?? testFailure("test did not finish before its parent and was cancelled", kCancelledByParent)); this.cancelled = true; this.abortController.abort(); } computeInheritedHooks(): void { if (this.parent!.hooks.beforeEach.length > 0) this.hooks.beforeEach.unshift(...this.parent!.hooks.beforeEach); if (this.parent!.hooks.afterEach.length > 0) this.hooks.afterEach.push(...this.parent!.hooks.afterEach); } createHook(name: HookName, fn: unknown, options: { loc?: [number, number, string] } & Record): void { if (!kHookNames.includes(name)) throw invalidArgValue("hook name", name, `must be one of: ${kHookNames.map((n) => `'${n}'`).join(", ")}`); const hook = new TestHook(fn as TestFn, options as unknown as TestOptions & { parent: Test; hookType: HookName }); if (name === "before" || name === "after") { const run = hook.run.bind(hook); let ran = false; let result: Promise | undefined; hook.run = (args?: RunArgs) => { if (!ran) { ran = true; result = run(args); } return result!; }; } if (name === "before" && this.startTime !== null) { void hook.run(this.getRunArgs()).then(() => { if (hook.error != null) this.fail(hook.error); }); } if (name === "afterEach") { this.hooks.afterEach.splice(this.hooks.ownAfterEachCount, 0, hook); this.hooks.ownAfterEachCount++; } else { this.hooks[name].push(hook); } } fail(err: unknown): void { if (this.error !== null) return; if (this.expectFailure) { if (this.expectFailure.match !== undefined) { const match = this.expectFailure.match; const e = err as { code?: string; failureType?: string; cause?: unknown }; const errorToCheck = e?.code === "ERR_TEST_FAILURE" && e?.failureType === kTestCodeFailure && e.cause !== undefined ? e.cause : err; const ok = match instanceof RegExp ? match.test(String((errorToCheck as Error)?.message ?? errorToCheck)) : typeof match === "function" ? Boolean((match as (e: unknown) => boolean)(errorToCheck)) : false; if (!ok) { this.passed = false; this.error = testFailure("The test failed, but the error did not match the expected validation", kTestCodeFailure); (this.error as { cause?: unknown }).cause = errorToCheck; return; } } this.passed = true; } else { this.passed = false; } this.error = err; } pass(): void { if (this.error == null && this.expectFailure && !this.skipped) { this.passed = false; this.error = testFailure("test was expected to fail but passed", kExpectedFailure); return; } if (this.error !== null) return; if (this.skipped || this.isTodo) { this.passed = true; return; } if (this.expectFailure) { this.passed = false; this.error = testFailure("Test passed but was expected to fail", kTestCodeFailure); return; } this.passed = true; } skip(message?: string): void { this.skipped = true; this.message = message ?? null; } todo(message?: string): void { this.isTodo = true; this.message = message ?? null; } diagnostic(message: string): void { this.diagnostics.push(message); } mock(): unknown { this.mockTracker ??= createMockTracker(); return this.mockTracker; } start(): Promise { this.applyFilters(); if (!this.filtered) this.testNumber = ++this.parent!.outputSubtestCount; this.parent!.unfinishedSubtests.add(this); this.reporter.enqueue(this.nesting, this.loc, this.name, this.reportedType, this.testId); // Node's test.js start(): `this.root.harness.buildPromise || !this.parent.hasConcurrency()`. // harness.ts's startSubtestAfterBootstrap already awaits (and nulls) buildPromise // before calling a top-level subtest's start(), so this branch of the check is // normally moot for a top-level call; it matters for a subtest started directly by // a Suite's own run() (its `subtests.map(s => s.start())`), which re-checks the same // shared root harness rather than assuming the gate already cleared. if (this.root.harness!.buildPromise || !this.parent!.hasConcurrency()) { let resolve!: () => void; const promise = new Promise((res) => (resolve = res)); this.parent!.addPendingSubtest({ test: this, resolve }); return promise; } this.parent!.assignReportOrder(this); this.reporter.dequeue(this.nesting, this.loc, this.name, this.reportedType, this.testId); return this.run(); } shouldAbort(): boolean { if (this.signal.aborted || this.outerSignal?.aborted) { this.#abortHandler(); return true; } return false; } getRunArgs(): RunArgs { const ctx = new TestContext(this); return { ctx, args: [ctx] }; } async runHook(hookName: HookName, args: RunArgs): Promise { try { const hooks = this.hooks[hookName]; for (const hook of hooks) { await hook.run(args); if (hook.error) throw hook.error; } } catch (err) { const error = testFailure(`failed running ${hookName} hook`, kHookFailure); error.cause = isTestFailureError(err) ? err.cause : err; throw error; } } async run(): Promise { if (this.parent !== null) { this.parent.activeSubtests++; this.computeInheritedHooks(); } this.startTime ??= performance.now(); if (this.shouldAbort()) { this.postRun(); return; } const hookArgs = this.getRunArgs(); const { args } = hookArgs; const ctx = hookArgs.ctx as TestContext; if (this.plan === null && this.expectedAssertions) ctx.plan(this.expectedAssertions); const wasSkippedBeforeRun = this.skipped; const after = async (): Promise => { if (this.hooks.after.length > 0) await this.runHook("after", hookArgs); }; let afterEachRan = false; let afterEachPromise: Promise | undefined; const afterEach = (): Promise => { if (!afterEachRan) { afterEachRan = true; afterEachPromise = (async () => { if ((this.parent?.hooks.afterEach.length ?? 0) > 0 && !wasSkippedBeforeRun) { await this.parent!.runHook("afterEach", hookArgs); } })(); } return afterEachPromise!; }; let stop: { promise: Promise; dispose(): void } | undefined; const previouslyRunning = currentlyRunning; currentlyRunning = this; try { if ((this.parent?.hooks.before.length ?? 0) > 0) await this.parent!.runHook("before", this.parent!.getRunArgs()); if ((this.parent?.hooks.beforeEach.length ?? 0) > 0 && !this.skipped) await this.parent!.runHook("beforeEach", hookArgs); stop = stopTest(this.timeout, this.signal); const runArgs: unknown[] = [...args]; const promises: Promise[] = []; if (this.fn.length === runArgs.length + 1) { // Legacy Node-style error-first callback. const { promise, cb } = createDeferredCallback(); const ret = this.fn(ctx, cb); if (ret && typeof (ret as Promise).then === "function") { this.fail(testFailure("passed a callback but also returned a Promise", kCallbackAndPromisePresent)); promises.push(Promise.resolve(ret)); } else { promises.push(promise); } } else { promises.push(Promise.resolve(this.fn(ctx))); } promises.push(stop.promise); await Promise.race(promises); this.shouldAbort(); if (this.subtestsPromise !== null) await Promise.race([this.subtestsPromise.promise, stop.promise]); if (this.plan !== null) { const planPromise = this.plan.check(); if (planPromise) await Promise.race([planPromise, stop.promise]); } this.pass(); await afterEach(); await after(); } catch (err) { if (isTestFailureError(err)) { if (err.failureType === kTestTimeoutFailure) this.#cancel(err); else this.fail(err); } else { this.fail(testFailure(err, kTestCodeFailure)); } try { await afterEach(); } catch { /* test is already failing */ } try { await after(); } catch { /* ignore */ } } finally { stop?.dispose(); currentlyRunning = previouslyRunning; if (this.parent !== null) this.abortController.abort(); } this.postRun(); } postRun(pendingSubtestsError?: unknown): void { this.endTime ??= performance.now(); this.startTime ??= this.endTime; this.pendingSubtests = []; let failed = 0; for (const subtest of this.subtests) { if (!subtest.finished) { subtest.#cancel(pendingSubtestsError); subtest.postRun(pendingSubtestsError); } if (!subtest.passed && !subtest.isTodo) failed++; } if ((this.passed || this.parent === null) && failed > 0) { const subtestString = `subtest${failed > 1 ? "s" : ""}`; this.fail(testFailure(`${failed} ${subtestString} failed`, kSubtestsFailed)); } this.outerSignal?.removeEventListener("abort", this.#abortHandler); // Node's postRun(): `this.mock?.reset()`, restoring every mock the test made and // disabling its mock timers, whether the test passed, failed or never mocked // anything (reset() is a no-op in that last case). this.mockTracker?.reset(); if (this.parent !== null) { if (!this.filtered) { const report = this.getReportDetails(); (report.details as { passed?: boolean }).passed = this.passed; this.testNumber ||= ++this.parent.outputSubtestCount; this.reporter.complete(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive, this.testId); this.parent.activeSubtests--; } this.parent.addReadySubtest(this); this.parent.processReadySubtestRange(false); void this.parent.processPendingSubtests(); } else if (!this.reported) { const { diagnostics, harness, loc, nesting, reporter } = this; this.reported = true; reporter.plan(nesting, loc, harness!.counters.topLevel); // Node's postRun(): write recorded snapshots before diagnostics, since a failed // write is itself reported as a diagnostic. harness!.snapshotManager?.writeSnapshotFiles(); for (const d of diagnostics) reporter.diagnostic(nesting, loc, d); const duration = this.duration(); reporter.diagnostic(nesting, loc, `tests ${harness!.counters.tests}`); reporter.diagnostic(nesting, loc, `suites ${harness!.counters.suites}`); reporter.diagnostic(nesting, loc, `pass ${harness!.counters.passed}`); reporter.diagnostic(nesting, loc, `fail ${harness!.counters.failed}`); reporter.diagnostic(nesting, loc, `cancelled ${harness!.counters.cancelled}`); reporter.diagnostic(nesting, loc, `skipped ${harness!.counters.skipped}`); reporter.diagnostic(nesting, loc, `todo ${harness!.counters.todo}`); reporter.diagnostic(nesting, loc, `duration_ms ${duration}`); reporter.summary(nesting, loc?.file, harness!.success, harness!.counters, duration); reporter.end(); } } finalize(): void { this.processReadySubtestRange(true); this.report(); this.parent!.waitingOn++; this.finished = true; // `waitingOn > subtests.length` means every top-level test has finished — safe to // rely on because no top-level test can even start until harness.ts's // startSubtestAfterBootstrap has resolved its `bootstrapPromise` wait, and (for // run()'s isolation:'none' path) that promise only resolves once every test file // has loaded and registered its own top-level tests. Give the root test's own // run() a chance to execute (it reports the final summary once it returns), same // as Node's harness invoking this from the last subtest's finalize() rather than // waiting for a separate 'beforeExit' turn. if (this.parent === this.root && this.root.waitingOn > this.root.subtests.length) { void this.root.run(); } } duration(): number { return (this.endTime! - this.startTime!); } getReportDetails(): { details: Record; directive: Record | undefined } { let directive: Record | undefined; const details: Record = { duration_ms: this.duration() }; if (this.skipped) directive = this.reporter.getSkip(this.message ?? undefined); else if (this.isTodo) directive = this.reporter.getTodo(this.message ?? undefined); else if (this.expectFailure) directive = this.reporter.getXFail(this.expectFailure.label); if (this.reportedType) details.type = this.reportedType; if (!this.passed) details.error = this.error; return { details, directive }; } report(): void { countCompletedTest(this); if (this.outputSubtestCount > 0) this.reporter.plan(this.subtests[0]!.nesting, this.loc, this.outputSubtestCount); else this.reportStarted(); const report = this.getReportDetails(); if (this.passed) this.reporter.ok(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive, this.testId); else this.reporter.fail(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive, this.testId); for (const d of this.diagnostics) this.reporter.diagnostic(this.nesting, this.loc, d); } reportStarted(): void { if (this.#reportedSubtest || this.parent === null) return; this.#reportedSubtest = true; this.parent.reportStarted(); this.reporter.start(this.nesting, this.loc, this.name, this.testId); } } export class TestHook extends Test { override reportedType: "test" | "suite" = "test"; parentTest: Test; #args: RunArgs | undefined; constructor(fn: TestFn, options: TestOptions & { parent: Test; hookType: HookName }) { const { hookType, loc, parent, timeout, signal } = options; super({ fn, loc, timeout, signal, harness: parent.root.harness! }); this.parentTest = parent; this.hookType = hookType; } override run(args?: RunArgs): Promise { if (args) this.#args = args; if (this.error && !this.outerSignal?.aborted) { this.passed = false; this.error = null; this.abortController.abort(); this.abortController = new AbortController(); this.signal = this.abortController.signal; } return super.run(); } override getRunArgs(): RunArgs { return this.#args!; } override postRun(): void { const { error, loc, parentTest: parent } = this; if (error && parent === parent.root && this.hookType === "after") { if (isTestFailureError(error)) error.failureType = kHookFailure; this.endTime ??= performance.now(); parent.reporter.fail(0, loc, parent.subtests.length + 1, loc!.file, { duration_ms: this.duration(), error }, undefined, 0); } } } export class Suite extends Test { override reportedType: "test" | "suite" = "suite"; buildSuite: Promise; constructor(options: TestOptions) { super(options); if (options.timeout == null) this.timeout = null; // Node's test.js also re-enables a `skip`ped suite's own fn here when BOTH // testNamePatterns and testSkipPatterns are non-null, so a name-filtered run can // still see inside it. Omitted: both fields are always null on every path this // kernel reaches today (see the file header), so the condition can never be true. this.buildSuite = this.createBuild(options.fn ?? noop); this.fn = noop; } async createBuild(fn: TestFn): Promise { try { const { ctx } = this.getRunArgs(); let result: unknown; pushBuildingParent(this); try { result = fn(ctx as never); } finally { popBuildingParent(); } await result; } catch (err) { this.fail(testFailure(err, kTestCodeFailure)); } finally { this.buildPhaseFinished = true; } } override getRunArgs(): RunArgs { const ctx = new SuiteContext(this); return { ctx, args: [ctx] }; } override async run(): Promise { this.computeInheritedHooks(); const hookArgs = this.getRunArgs(); let stop: { promise: Promise; dispose(): void } | undefined; let afterRan = false; const after = async (): Promise => { if (!afterRan) { afterRan = true; await this.runHook("after", hookArgs); } }; try { this.parent!.activeSubtests++; await this.buildSuite; this.startTime = performance.now(); if (this.shouldAbort()) { this.subtests = []; this.postRun(); return; } if (this.parent!.hooks.before.length > 0) await this.parent!.runHook("before", this.parent!.getRunArgs()); await this.runHook("before", hookArgs); stop = stopTest(this.timeout, this.signal); const subtests = this.skipped || this.error ? [] : this.subtests; const promise = Promise.all(subtests.map((s) => s.start())); await Promise.race([promise, stop.promise]); await after(); this.pass(); } catch (err) { try { await after(); } catch { /* suite is already failing */ } if (isTestFailureError(err)) this.fail(err); else this.fail(testFailure(err, kTestCodeFailure)); } finally { stop?.dispose(); } this.postRun(); } } export { getFullName };