diff --git a/scripts/node_suite_run.py b/scripts/node_suite_run.py index a95778c34..301e23817 100755 --- a/scripts/node_suite_run.py +++ b/scripts/node_suite_run.py @@ -48,6 +48,8 @@ _DUR_MS = re.compile(r"(?<=: )\d+(?:\.\d+)?(ms|s)\b") _DUR_CLOCK = re.compile(r"\b\d+:\d{2}\.\d{3} \((h:mm|m):ss\.mmm\)") _FRAME = re.compile(r"^\s{4,}(at\s|\d+:\s)|… \d+ more identical frames") +_TEST_DURATION = re.compile(r"\(\d+(?:\.\d+)?(?:ms|s)\)") +_TEST_LOCATION = re.compile(r"^(\s*)test at .+:\d+:\d+$") def normalize(text: str) -> str: @@ -55,8 +57,16 @@ def normalize(text: str) -> str: for line in text.split("\n"): if _FRAME.search(line): continue + location = _TEST_LOCATION.match(line) + if location: + line = f"{location.group(1)}test at " line = _DUR_MS.sub(lambda m: "" + m.group(1), line) line = _DUR_CLOCK.sub(lambda m: "", line) + line = _TEST_DURATION.sub("()", line) + stripped = line.lstrip() + if stripped.startswith("ℹ duration_ms "): + indent = line[: len(line) - len(stripped)] + line = f"{indent}ℹ duration_ms " out.append(line) return "\n".join(out) diff --git a/test-parity/node-suite/test/README.md b/test-parity/node-suite/test/README.md new file mode 100644 index 000000000..bceef8e3f --- /dev/null +++ b/test-parity/node-suite/test/README.md @@ -0,0 +1,70 @@ +# `node:test` granular parity coverage + +This directory compares deterministic `node:test` semantics through controlled +console markers. The differential runner still compares the test runner's own +stdout and exit status, but canonicalizes test durations and source locations; +fixtures do not assert paths, stacks, process IDs, or terminal formatting. + +## Upstream selection + +The expansion was selected on 2026-07-16 from these primary repository +snapshots: + +- Node.js [`34c28d5a69f4f00cd599adcbe57834435d3a683b`](https://github.com/nodejs/node/tree/34c28d5a69f4f00cd599adcbe57834435d3a683b), especially + [`test-runner-mocking.js`](https://github.com/nodejs/node/blob/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/parallel/test-runner-mocking.js), + [`test-runner-plan.mjs`](https://github.com/nodejs/node/blob/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/parallel/test-runner-plan.mjs), + [`test-runner-aftereach-runtime-skip.js`](https://github.com/nodejs/node/blob/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/parallel/test-runner-aftereach-runtime-skip.js), + [`test-runner-subtest-after-hook.js`](https://github.com/nodejs/node/blob/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/parallel/test-runner-subtest-after-hook.js), and the deterministic parts of the run and mock-timer tests. +- Deno [`f8a17c8171569fa2870d740030aaa59c91fdf9ee`](https://github.com/denoland/deno/tree/f8a17c8171569fa2870d740030aaa59c91fdf9ee). Deno's current Node-compat selection does not carry a dedicated `node:test` file under `tests/unit_node`; its runner, context, hooks, mocks, timer, snapshot, and reporter compatibility lives in + [`ext/node/polyfills/testing.ts`](https://github.com/denoland/deno/blob/f8a17c8171569fa2870d740030aaa59c91fdf9ee/ext/node/polyfills/testing.ts). +- Bun [`6173d6431ee8ad086bf79d1d5354080cfe937964`](https://github.com/oven-sh/bun/tree/6173d6431ee8ad086bf79d1d5354080cfe937964), especially its + [`node:test` selection](https://github.com/oven-sh/bun/blob/6173d6431ee8ad086bf79d1d5354080cfe937964/test/js/node/test_runner/node-test.test.ts) and + [hook-order fixture](https://github.com/oven-sh/bun/blob/6173d6431ee8ad086bf79d1d5354080cfe937964/test/js/node/test_runner/fixtures/02-hooks.js). + +## Added diagnostic categories + +- `imports` and `runner/registration` (10 fixtures): export aliases, callback + overloads, deferred registration, async/callback completion, option + validation, nested suites/subtests, and parent-child completion. +- `runner/context` and `runner/api`: assertion surface, plans, runtime + skip/todo/only, failure propagation, names, the existing diagnostic case, and + the claimed `run()` surface (16 fixtures). +- `runner/hooks` (9 fixtures): global, repeated, and nested ordering, hook + context, runtime-skip cleanup, and cleanup after body or setup throws. +- `mock-fn` (27 fixtures): successful, async, bound, no-op, and throwing call + records; receiver/prototype/inheritance behavior; implementation queues and + indexed overrides; reset/restore behavior; property/accessor contexts; + symbols, descriptors, `times`, and validation. +- `mock-timers` (13 fixtures): timeout arguments and cancellation, interval + repetition/self-clear, reset, validation, boundary ordering, nested + `runAll()`, Date construction, and deterministic `setTime()` behavior. +- `snapshots` and `reporters` (4 fixtures): serializer/assertion validation and + synthetic directive/nesting events. + +Snapshot fixtures use fixed local files, and reporter fixtures feed synthetic +events. The 79 added fixtures expand the module from 11 to 90 cases without +depending on wall-clock time or external files. + +## Stopping judgment + +The remaining Node runner corpus is intentionally left for separate work: + +- CLI discovery, watch mode, coverage, source maps, process isolation, worker + IDs, global setup, rerun state, and force-exit behavior require subprocess or + multi-file harness support rather than this in-process granular suite. +- concurrency, randomization, timeouts, abort scheduling, refed handles, and + scheduler-sensitive timer APIs are not deterministic enough for byte-for-byte + stdout comparison here. +- enabling mock timers twice currently leaves a Perry process handle alive even + after `reset()`; that validation belongs with the runtime cleanup fix rather + than a granular case that times out the harness. +- colors/TTY, absolute locations, stacks, durations, and reporter formatting + tied to those values are environment-specific. +- `TestContext.waitFor()`, `runOnly()`, tags, full names, signals, and custom + assertions are not listed in Perry's current `node:test` manifest; testing + those as claimed compatibility would overstate the supported surface. +- module mocking and snapshot update/CLI behavior are separate runtime and CLI + projects. Constructor-target mocks are also deferred because Perry does not + currently expose constructable mock wrappers. Further core cases are + redundant with the focused fixtures above or depend on one of these excluded + surfaces. diff --git a/test-parity/node-suite/test/imports/aliases.ts b/test-parity/node-suite/test/imports/aliases.ts new file mode 100644 index 000000000..a7bad7c70 --- /dev/null +++ b/test-parity/node-suite/test/imports/aliases.ts @@ -0,0 +1,6 @@ +import test, { describe, it, suite } from "node:test"; + +console.log("test.test identity:", test.test === test); +console.log("it identity:", it === test); +console.log("describe identity:", describe === suite); +console.log("method identities:", test.skip === test.skip, test.todo === test.todo); diff --git a/test-parity/node-suite/test/mock-fn/accessor-options.ts b/test-parity/node-suite/test/mock-fn/accessor-options.ts new file mode 100644 index 000000000..596cffcf2 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/accessor-options.ts @@ -0,0 +1,18 @@ +import { mock } from "node:test"; + +let stored = "initial"; +const target = { + get value() { + return stored; + }, + set value(value: string) { + stored = value; + }, +}; + +const getter = mock.getter(target, "value", {}); +const setter = mock.setter(target, "value", {}); +console.log("getter options:", target.value, getter.mock.callCount()); +target.value = "changed"; +console.log("setter options:", stored, setter.mock.callCount()); +mock.restoreAll(); diff --git a/test-parity/node-suite/test/mock-fn/accessor-preservation.ts b/test-parity/node-suite/test/mock-fn/accessor-preservation.ts new file mode 100644 index 000000000..708ee15f1 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/accessor-preservation.ts @@ -0,0 +1,24 @@ +import { mock } from "node:test"; + +let stored = 1; +const target = { + get value() { + return stored; + }, + set value(value: number) { + stored = value; + }, +}; + +const getter = mock.getter(target, "value", () => 10); +target.value = 4; +console.log("getter preserves setter:", target.value, stored); +getter.mock.restore(); + +const setter = mock.setter(target, "value", (value: number) => { + stored = value * 2; +}); +console.log("setter preserves getter:", target.value); +target.value = 5; +console.log("setter result:", stored, target.value); +setter.mock.restore(); diff --git a/test-parity/node-suite/test/mock-fn/async-result.ts b/test-parity/node-suite/test/mock-fn/async-result.ts new file mode 100644 index 000000000..56be88c98 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/async-result.ts @@ -0,0 +1,7 @@ +import { mock } from "node:test"; + +const fn = mock.fn(async (value: number) => value * 2); +const promise = fn(4); +console.log("record promise:", fn.mock.calls[0].result === promise, fn.mock.callCount()); +console.log("resolved:", await promise); +mock.restoreAll(); diff --git a/test-parity/node-suite/test/mock-fn/bound.ts b/test-parity/node-suite/test/mock-fn/bound.ts new file mode 100644 index 000000000..3bab59f00 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/bound.ts @@ -0,0 +1,10 @@ +import { mock } from "node:test"; + +const fn = mock.fn(function (this: any, a: number, b: number) { + return this.offset + a + b; +}); +const bound = fn.bind({ offset: 10 }, 2); +console.log("bound result:", bound(3)); +console.log("bound count:", fn.mock.callCount(), JSON.stringify(fn.mock.calls[0].arguments)); +console.log("bound this:", fn.mock.calls[0].this.offset); +mock.restoreAll(); diff --git a/test-parity/node-suite/test/mock-fn/call-record-errors.ts b/test-parity/node-suite/test/mock-fn/call-record-errors.ts new file mode 100644 index 000000000..014d44f66 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/call-record-errors.ts @@ -0,0 +1,33 @@ +import { mock } from "node:test"; + +const expected = new Error("controlled mock failure"); +const fn = mock.fn(function (this: any, value: number) { + if (value < 0) throw expected; + return this.offset + value; +}); + +console.log("success:", fn.call({ label: "receiver", offset: 4 }, 3)); +try { + fn(-1); +} catch (error) { + console.log("caught:", error === expected); +} + +const success = fn.mock.calls[0]; +const failure = fn.mock.calls[1]; +console.log( + "success record:", + JSON.stringify(success.arguments), + success.this.label, + success.result, + success.error === undefined, + success.target === undefined, +); +console.log( + "failure record:", + JSON.stringify(failure.arguments), + failure.result === undefined, + failure.error === expected, + typeof failure.stack, +); +mock.restoreAll(); diff --git a/test-parity/node-suite/test/mock-fn/call-record-order.ts b/test-parity/node-suite/test/mock-fn/call-record-order.ts new file mode 100644 index 000000000..b83f47e91 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/call-record-order.ts @@ -0,0 +1,11 @@ +import { mock } from "node:test"; + +let inside = -1; +const fn = mock.fn(() => { + inside = fn.mock.callCount(); + return "value"; +}); + +console.log("call result:", fn()); +console.log("counts:", inside, fn.mock.callCount()); +mock.restoreAll(); diff --git a/test-parity/node-suite/test/mock-fn/idempotent-restore.ts b/test-parity/node-suite/test/mock-fn/idempotent-restore.ts new file mode 100644 index 000000000..d7b129af2 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/idempotent-restore.ts @@ -0,0 +1,13 @@ +import { mock } from "node:test"; + +const target = { + read() { + return "original"; + }, +}; +const method = mock.method(target, "read", () => "mocked"); +console.log("mocked:", target.read()); +method.mock.restore(); +console.log("first restore:", target.read()); +method.mock.restore(); +console.log("second restore:", target.read()); diff --git a/test-parity/node-suite/test/mock-fn/implementations.ts b/test-parity/node-suite/test/mock-fn/implementations.ts new file mode 100644 index 000000000..494d4a242 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/implementations.ts @@ -0,0 +1,13 @@ +import { mock } from "node:test"; + +const fn = mock.fn((value: number) => `original:${value}`); +fn.mock.mockImplementation((value: number) => `replacement:${value}`); +fn.mock.mockImplementationOnce((value: number) => `once-a:${value}`); +fn.mock.mockImplementationOnce((value: number) => `once-b:${value}`); + +console.log("sequence:", fn(1), fn(2), fn(3)); +console.log("count:", fn.mock.callCount(), fn.mock.calls.length); +fn.mock.resetCalls(); +console.log("reset calls:", fn.mock.callCount(), fn(4)); +fn.mock.restore(); +console.log("restore:", fn(5), fn.mock.callCount()); diff --git a/test-parity/node-suite/test/mock-fn/inherited-method.ts b/test-parity/node-suite/test/mock-fn/inherited-method.ts new file mode 100644 index 000000000..c9ec88d9e --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/inherited-method.ts @@ -0,0 +1,12 @@ +import { mock } from "node:test"; + +const prototype = { + read() { + return "prototype"; + }, +}; +const target = Object.create(prototype); +const method = mock.method(target, "read", () => "mocked"); +console.log("inherited mocked:", target.read(), Object.hasOwn(target, "read")); +method.mock.restore(); +console.log("inherited restored:", target.read(), Object.hasOwn(target, "read")); diff --git a/test-parity/node-suite/test/mock-fn/method-options-validation.ts b/test-parity/node-suite/test/mock-fn/method-options-validation.ts new file mode 100644 index 000000000..c6a91096f --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/method-options-validation.ts @@ -0,0 +1,19 @@ +import { mock } from "node:test"; + +function codeOf(fn: () => void): string { + try { + fn(); + return "NO_THROW"; + } catch (error) { + return (error as any).code ?? (error as Error).name; + } +} + +const target = { method() {} }; +console.log("options number:", codeOf(() => mock.method(target, "method", () => {}, 5 as any))); +console.log( + "getter setter:", + codeOf(() => mock.method(target, "method", { getter: true, setter: true } as any)), +); +console.log("getter false:", codeOf(() => mock.getter(target, "method", { getter: false } as any))); +mock.restoreAll(); diff --git a/test-parity/node-suite/test/mock-fn/method-receiver.ts b/test-parity/node-suite/test/mock-fn/method-receiver.ts new file mode 100644 index 000000000..ed9d4855a --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/method-receiver.ts @@ -0,0 +1,14 @@ +import { mock } from "node:test"; + +const target = { + base: 4, + add(value: number) { + return this.base + value; + }, +}; + +const method = mock.method(target, "add"); +console.log("method result:", target.add(3)); +const call = method.mock.calls[0]; +console.log("method record:", call.this === target, JSON.stringify(call.arguments), call.result); +method.mock.restore(); diff --git a/test-parity/node-suite/test/mock-fn/method-restore-chain.ts b/test-parity/node-suite/test/mock-fn/method-restore-chain.ts new file mode 100644 index 000000000..490fe11b0 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/method-restore-chain.ts @@ -0,0 +1,16 @@ +import { mock } from "node:test"; + +const target = { + value: 10, + read() { + return this.value; + }, +}; + +const first = mock.method(target, "read", () => 20); +const second = mock.method(target, "read", () => 30); +console.log("nested:", target.read(), first === second); +second.mock.restore(); +console.log("restore second:", target.read(), target.read === first); +first.mock.restore(); +console.log("restore first:", target.read(), (target.read as any).mock === undefined); diff --git a/test-parity/node-suite/test/mock-fn/name-length.ts b/test-parity/node-suite/test/mock-fn/name-length.ts new file mode 100644 index 000000000..c54932566 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/name-length.ts @@ -0,0 +1,15 @@ +import { mock } from "node:test"; + +function original(a: unknown, b: unknown) { + return [a, b]; +} + +const fn = mock.fn(original); +console.log("name:", original.name, fn.name); +console.log("length:", original.length, fn.length); +console.log( + "descriptors:", + Object.getOwnPropertyDescriptor(fn, "name")?.configurable, + Object.getOwnPropertyDescriptor(fn, "length")?.configurable, +); +mock.restoreAll(); diff --git a/test-parity/node-suite/test/mock-fn/no-op.ts b/test-parity/node-suite/test/mock-fn/no-op.ts new file mode 100644 index 000000000..4fdc760f4 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/no-op.ts @@ -0,0 +1,15 @@ +import { mock } from "node:test"; + +const fn = mock.fn(); +console.log("initial:", fn.mock.callCount(), fn.mock.calls.length); +console.log("result:", fn("a", 2)); +const call = fn.mock.calls[0]; +console.log( + "record:", + JSON.stringify(call.arguments), + call.result === undefined, + call.error === undefined, + call.this === undefined, + call.target === undefined, +); +mock.restoreAll(); diff --git a/test-parity/node-suite/test/mock-fn/non-configurable.ts b/test-parity/node-suite/test/mock-fn/non-configurable.ts new file mode 100644 index 000000000..70acaf868 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/non-configurable.ts @@ -0,0 +1,16 @@ +import { mock } from "node:test"; + +const target: any = {}; +Object.defineProperty(target, "method", { + configurable: false, + value() { + return "original"; + }, +}); + +try { + mock.method(target, "method", () => "mocked"); + console.log("non-configurable: NO_THROW", target.method()); +} catch (error) { + console.log("non-configurable:", (error as any).code ?? (error as Error).name, target.method()); +} diff --git a/test-parity/node-suite/test/mock-fn/once-indices.ts b/test-parity/node-suite/test/mock-fn/once-indices.ts new file mode 100644 index 000000000..ee9a7a1be --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/once-indices.ts @@ -0,0 +1,9 @@ +import { mock } from "node:test"; + +const fn = mock.fn(() => "default"); +fn(); +fn.mock.mockImplementationOnce(() => "third", 2); +fn.mock.mockImplementationOnce(() => "fifth", 4); +console.log("indexed:", fn(), fn(), fn(), fn(), fn()); +console.log("count:", fn.mock.callCount()); +mock.restoreAll(); diff --git a/test-parity/node-suite/test/mock-fn/property-access-records.ts b/test-parity/node-suite/test/mock-fn/property-access-records.ts new file mode 100644 index 000000000..8d49ae7da --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/property-access-records.ts @@ -0,0 +1,15 @@ +import { mock } from "node:test"; + +const target = { value: 1 }; +const property = mock.property(target, "value", 2); +console.log("get:", target.value); +target.value = 3; +console.log("set-get:", target.value); +console.log( + "accesses:", + property.mock.accessCount(), + property.mock.accesses.map((access: any) => `${access.type}:${access.value}`).join(","), +); +property.mock.resetAccesses(); +console.log("reset accesses:", property.mock.accessCount(), target.value, property.mock.accessCount()); +property.mock.restore(); diff --git a/test-parity/node-suite/test/mock-fn/property-context.ts b/test-parity/node-suite/test/mock-fn/property-context.ts new file mode 100644 index 000000000..73a8c13f2 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/property-context.ts @@ -0,0 +1,11 @@ +import { mock } from "node:test"; + +const target = { value: 1 }; +const property = mock.property(target, "value", 2); +console.log("initial:", target.value, typeof property.mock); +property.mock.mockImplementation(3); +console.log("replacement:", target.value); +property.mock.mockImplementationOnce(4); +console.log("once:", target.value, target.value); +property.mock.restore(); +console.log("restored:", target.value); diff --git a/test-parity/node-suite/test/mock-fn/property-no-value.ts b/test-parity/node-suite/test/mock-fn/property-no-value.ts new file mode 100644 index 000000000..dea654464 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/property-no-value.ts @@ -0,0 +1,9 @@ +import { mock } from "node:test"; + +const target = { value: 5 }; +const property = mock.property(target, "value"); +console.log("initial spy:", target.value, property.mock.accessCount()); +target.value = 6; +console.log("updated spy:", target.value, property.mock.accessCount()); +property.mock.restore(); +console.log("restored:", target.value); diff --git a/test-parity/node-suite/test/mock-fn/property-on-access.ts b/test-parity/node-suite/test/mock-fn/property-on-access.ts new file mode 100644 index 000000000..302a59a3f --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/property-on-access.ts @@ -0,0 +1,10 @@ +import { mock } from "node:test"; + +const target = { value: 1 }; +const property = mock.property(target, "value", 2); +property.mock.mockImplementation(9); +property.mock.mockImplementationOnce(4, 1); +property.mock.mockImplementationOnce(6, 3); +console.log("access sequence:", target.value, target.value, target.value, target.value); +console.log("access count:", property.mock.accessCount()); +property.mock.restore(); diff --git a/test-parity/node-suite/test/mock-fn/property-undefined.ts b/test-parity/node-suite/test/mock-fn/property-undefined.ts new file mode 100644 index 000000000..144ff2db9 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/property-undefined.ts @@ -0,0 +1,7 @@ +import { mock } from "node:test"; + +const target: { value: number | undefined } = { value: 5 }; +const property = mock.property(target, "value", undefined); +console.log("mocked undefined:", target.value === undefined, property.mock.accessCount()); +property.mock.restore(); +console.log("restored value:", target.value); diff --git a/test-parity/node-suite/test/mock-fn/prototype-method.ts b/test-parity/node-suite/test/mock-fn/prototype-method.ts new file mode 100644 index 000000000..c6e2756b6 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/prototype-method.ts @@ -0,0 +1,15 @@ +import { mock } from "node:test"; + +class Counter { + constructor(public value: number) {} + read() { + return this.value; + } +} + +const instance = new Counter(7); +const method = mock.method(Counter.prototype, "read"); +console.log("prototype result:", instance.read()); +console.log("prototype record:", method.mock.callCount(), method.mock.calls[0].this === instance); +method.mock.restore(); +console.log("prototype restore:", instance.read()); diff --git a/test-parity/node-suite/test/mock-fn/reset-multiple.ts b/test-parity/node-suite/test/mock-fn/reset-multiple.ts new file mode 100644 index 000000000..ec8a68c8f --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/reset-multiple.ts @@ -0,0 +1,12 @@ +import { mock } from "node:test"; + +const first = mock.fn(() => "first"); +const second = mock.fn(() => "second"); +first(); +second(); +second(); +console.log("before reset:", first.mock.callCount(), second.mock.callCount()); +mock.reset(); +console.log("after reset:", first.mock.callCount(), second.mock.callCount()); +console.log("after calls:", first(), second(), first.mock.callCount(), second.mock.callCount()); +mock.restoreAll(); diff --git a/test-parity/node-suite/test/mock-fn/symbol-method.ts b/test-parity/node-suite/test/mock-fn/symbol-method.ts new file mode 100644 index 000000000..8f5ab142f --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/symbol-method.ts @@ -0,0 +1,17 @@ +import { mock } from "node:test"; + +const key = Symbol("method"); +const target = { + [key](value: number) { + return value + 1; + }, +}; + +try { + const method = mock.method(target, key, (value: number) => value * 2); + console.log("symbol method:", target[key](3), method.mock.callCount()); + method.mock.restore(); + console.log("symbol restore:", target[key](3)); +} catch (error) { + console.log("symbol error:", (error as any).code ?? (error as Error).name); +} diff --git a/test-parity/node-suite/test/mock-fn/times-options.ts b/test-parity/node-suite/test/mock-fn/times-options.ts new file mode 100644 index 000000000..5beaa2719 --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/times-options.ts @@ -0,0 +1,18 @@ +import { mock } from "node:test"; + +const fn = mock.fn( + () => "original", + () => "temporary", + { times: 2 }, +); +console.log("fn times:", fn(), fn(), fn(), fn.mock.callCount()); + +const target = { + value: 5, + read() { + return this.value; + }, +}; +mock.method(target, "read", () => 99, { times: 1 }); +console.log("method times:", target.read(), target.read()); +mock.restoreAll(); diff --git a/test-parity/node-suite/test/mock-fn/tracker-reset-restore.ts b/test-parity/node-suite/test/mock-fn/tracker-reset-restore.ts new file mode 100644 index 000000000..5b657b98c --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/tracker-reset-restore.ts @@ -0,0 +1,18 @@ +import { mock } from "node:test"; + +const fn = mock.fn( + () => "original", + () => "replacement", +); + +console.log("initial:", fn()); +mock.restoreAll(); +console.log("after restoreAll:", fn()); +fn.mock.mockImplementation(() => "again"); +console.log("replaced again:", fn()); +mock.reset(); +console.log("after reset:", fn(), fn.mock.callCount()); +fn.mock.mockImplementation(() => "post-reset"); +mock.restoreAll(); +console.log("disassociated:", fn()); +mock.reset(); diff --git a/test-parity/node-suite/test/mock-fn/validation.ts b/test-parity/node-suite/test/mock-fn/validation.ts new file mode 100644 index 000000000..53dd4345b --- /dev/null +++ b/test-parity/node-suite/test/mock-fn/validation.ts @@ -0,0 +1,23 @@ +import { mock } from "node:test"; + +function codeOf(fn: () => void): string { + try { + fn(); + return "NO_THROW"; + } catch (error) { + return (error as any).code ?? (error as Error).name; + } +} + +console.log("fn original:", codeOf(() => mock.fn(1 as any))); +console.log("method target:", codeOf(() => mock.method(null as any, "x"))); +console.log("method missing:", codeOf(() => mock.method({}, "x"))); +console.log("getter missing:", codeOf(() => mock.getter({}, "x"))); +console.log( + "times zero:", + codeOf(() => mock.fn(() => undefined, { times: 0 })), +); +console.log( + "times fraction:", + codeOf(() => mock.fn(() => undefined, { times: 1.5 })), +); diff --git a/test-parity/node-suite/test/mock-timers/clear-timeout.ts b/test-parity/node-suite/test/mock-timers/clear-timeout.ts new file mode 100644 index 000000000..a801d8fe9 --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/clear-timeout.ts @@ -0,0 +1,12 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["setTimeout"], now: 0 }); +const events: string[] = []; +const cancelled = setTimeout(() => events.push("cancelled"), 5); +setTimeout(() => events.push("kept"), 5); +clearTimeout(cancelled); +clearTimeout(undefined as any); +clearTimeout(null as any); +mock.timers.tick(5); +console.log("clear timeout:", JSON.stringify(events)); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/mock-timers/date-constructors.ts b/test-parity/node-suite/test/mock-timers/date-constructors.ts new file mode 100644 index 000000000..bb62c44a1 --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/date-constructors.ts @@ -0,0 +1,8 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["Date"], now: 100 }); +console.log("no args:", new Date().getTime()); +console.log("numeric args:", new Date(0).getTime(), new Date(250).getTime()); +console.log("string arg:", new Date("1970-01-01T00:00:00.000Z").getTime()); +console.log("static methods:", Date.parse("1970-01-01T00:00:00.000Z"), Date.UTC(1970, 0, 1)); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/mock-timers/date-set-time.ts b/test-parity/node-suite/test/mock-timers/date-set-time.ts new file mode 100644 index 000000000..e56ec1e57 --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/date-set-time.ts @@ -0,0 +1,9 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["Date"], now: 1_000 }); +console.log("initial:", Date.now(), new Date().toISOString()); +mock.timers.setTime(2_500); +console.log("advanced:", Date.now(), new Date().toISOString()); +mock.timers.setTime(500); +console.log("rewound:", Date.now(), new Date().toISOString()); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/mock-timers/default-epoch.ts b/test-parity/node-suite/test/mock-timers/default-epoch.ts new file mode 100644 index 000000000..a738228a9 --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/default-epoch.ts @@ -0,0 +1,5 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["Date"] }); +console.log("default epoch:", Date.now() === 0, new Date().getTime() === 0); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/mock-timers/default-tick.ts b/test-parity/node-suite/test/mock-timers/default-tick.ts new file mode 100644 index 000000000..1fd552a1a --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/default-tick.ts @@ -0,0 +1,17 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["setTimeout"], now: 0 }); +const events: string[] = []; +setTimeout(() => events.push("one"), 1); +setTimeout(() => events.push("two"), 2); +function tick(): string { + try { + mock.timers.tick(); + return "OK"; + } catch (error) { + return (error as any).code ?? (error as Error).name; + } +} +console.log("default tick:", tick(), JSON.stringify(events)); +console.log("second tick:", tick(), JSON.stringify(events)); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/mock-timers/interval-repeated.ts b/test-parity/node-suite/test/mock-timers/interval-repeated.ts new file mode 100644 index 000000000..ea18e2d1b --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/interval-repeated.ts @@ -0,0 +1,11 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["setInterval"], now: 0 }); +const events: number[] = []; +const interval = setInterval((value: number) => events.push(value), 2, 7); +mock.timers.tick(1); +mock.timers.tick(1); +mock.timers.tick(4); +clearInterval(interval); +console.log("interval repeated:", JSON.stringify(events)); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/mock-timers/interval-self-clear.ts b/test-parity/node-suite/test/mock-timers/interval-self-clear.ts new file mode 100644 index 000000000..701a43c2b --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/interval-self-clear.ts @@ -0,0 +1,14 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["setInterval"], now: 0 }); +const events: string[] = []; +const first = setInterval(() => events.push("first"), 2); +const second = setInterval(() => { + events.push("second"); + clearInterval(second); +}, 2); +mock.timers.tick(2); +mock.timers.tick(2); +clearInterval(first); +console.log("self clear:", JSON.stringify(events)); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/mock-timers/reset-cancels.ts b/test-parity/node-suite/test/mock-timers/reset-cancels.ts new file mode 100644 index 000000000..d79b0d8f9 --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/reset-cancels.ts @@ -0,0 +1,15 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["setTimeout"], now: 0 }); +let called = false; +setTimeout(() => { + called = true; +}, 5); +mock.timers.reset(); +console.log("reset cancelled:", called); +try { + mock.timers.tick(5); + console.log("tick after reset: NO_THROW"); +} catch (error) { + console.log("tick after reset:", (error as any).code ?? (error as Error).name); +} diff --git a/test-parity/node-suite/test/mock-timers/run-all-nested.ts b/test-parity/node-suite/test/mock-timers/run-all-nested.ts new file mode 100644 index 000000000..9daf838f5 --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/run-all-nested.ts @@ -0,0 +1,12 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["Date", "setTimeout"], now: 100 }); +const events: string[] = []; +setTimeout(() => { + events.push(`outer:${Date.now()}`); + setTimeout(() => events.push(`inner:${Date.now()}`), 5); +}, 10); + +mock.timers.runAll(); +console.log("runAll:", JSON.stringify(events)); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/mock-timers/set-time-no-fire.ts b/test-parity/node-suite/test/mock-timers/set-time-no-fire.ts new file mode 100644 index 000000000..eb51294cd --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/set-time-no-fire.ts @@ -0,0 +1,11 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["Date", "setTimeout"], now: 0 }); +const events: string[] = []; +const timeout = setTimeout(() => events.push("timeout"), 10); +mock.timers.setTime(20); +console.log("after setTime:", Date.now(), JSON.stringify(events)); +mock.timers.tick(1); +console.log("after tick:", Date.now(), JSON.stringify(events)); +clearTimeout(timeout); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/mock-timers/tick-boundary-order.ts b/test-parity/node-suite/test/mock-timers/tick-boundary-order.ts new file mode 100644 index 000000000..6fd6658af --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/tick-boundary-order.ts @@ -0,0 +1,15 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["setTimeout"], now: 0 }); +const events: string[] = []; +setTimeout(() => events.push("late"), 10); +setTimeout(() => events.push("first-at-five"), 5); +setTimeout(() => events.push("second-at-five"), 5); + +mock.timers.tick(4); +console.log("tick4:", JSON.stringify(events)); +mock.timers.tick(1); +console.log("tick5:", JSON.stringify(events)); +mock.timers.tick(5); +console.log("tick10:", JSON.stringify(events)); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/mock-timers/timeout-arguments.ts b/test-parity/node-suite/test/mock-timers/timeout-arguments.ts new file mode 100644 index 000000000..9f9053ac7 --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/timeout-arguments.ts @@ -0,0 +1,8 @@ +import { mock } from "node:test"; + +mock.timers.enable({ apis: ["setTimeout"], now: 0 }); +const calls: unknown[][] = []; +setTimeout((...args: unknown[]) => calls.push(args), 5, "a", 2, true); +mock.timers.tick(5); +console.log("timeout args:", JSON.stringify(calls)); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/mock-timers/validation.ts b/test-parity/node-suite/test/mock-timers/validation.ts new file mode 100644 index 000000000..6f69fbc0f --- /dev/null +++ b/test-parity/node-suite/test/mock-timers/validation.ts @@ -0,0 +1,21 @@ +import { mock } from "node:test"; + +function codeOf(fn: () => void): string { + try { + fn(); + return "NO_THROW"; + } catch (error) { + return (error as any).code ?? (error as Error).name; + } +} + +console.log("runAll disabled:", codeOf(() => mock.timers.runAll())); +console.log("tick disabled:", codeOf(() => mock.timers.tick())); +console.log("setTime disabled:", codeOf(() => mock.timers.setTime(1))); +console.log("bad options:", codeOf(() => mock.timers.enable(null as any))); +console.log("bad api type:", codeOf(() => mock.timers.enable({ apis: [1 as any] }))); +console.log("bad now:", codeOf(() => mock.timers.enable({ now: -1 }))); +mock.timers.enable({ apis: ["Date"], now: 0 }); +console.log("negative tick:", codeOf(() => mock.timers.tick(-1))); +console.log("infinite tick:", codeOf(() => mock.timers.tick(Infinity))); +mock.timers.reset(); diff --git a/test-parity/node-suite/test/reporters/directives.ts b/test-parity/node-suite/test/reporters/directives.ts new file mode 100644 index 000000000..a08580b76 --- /dev/null +++ b/test-parity/node-suite/test/reporters/directives.ts @@ -0,0 +1,31 @@ +import { Readable } from "node:stream"; +import { spec, tap } from "node:test/reporters"; + +const events = [ + { type: "test:start", data: { name: "skipped", nesting: 0 } }, + { + type: "test:pass", + data: { name: "skipped", nesting: 0, skip: "reason", details: { type: "test" } }, + }, + { type: "test:start", data: { name: "todo", nesting: 0 } }, + { + type: "test:pass", + data: { name: "todo", nesting: 0, todo: "later", details: { type: "test" } }, + }, + { + type: "test:summary", + data: { + counts: { tests: 2, suites: 0, pass: 0, fail: 0, cancelled: 0, skipped: 1, todo: 1 }, + duration_ms: 1, + }, + }, +]; + +async function collect(name: string, reporter: any) { + let output = ""; + for await (const chunk of reporter(Readable.from(events))) output += String(chunk); + console.log(`${name}:`, JSON.stringify(output)); +} + +await collect("spec", spec); +await collect("tap", tap); diff --git a/test-parity/node-suite/test/reporters/nested.ts b/test-parity/node-suite/test/reporters/nested.ts new file mode 100644 index 000000000..e5b74ed6b --- /dev/null +++ b/test-parity/node-suite/test/reporters/nested.ts @@ -0,0 +1,24 @@ +import { Readable } from "node:stream"; +import { spec, tap } from "node:test/reporters"; + +const events = [ + { type: "test:start", data: { name: "parent", nesting: 0 } }, + { type: "test:start", data: { name: "child", nesting: 1 } }, + { + type: "test:pass", + data: { name: "child", nesting: 1, duration_ms: 1, details: { type: "test" } }, + }, + { + type: "test:pass", + data: { name: "parent", nesting: 0, duration_ms: 2, details: { type: "suite" } }, + }, +]; + +async function collect(name: string, reporter: any) { + let output = ""; + for await (const chunk of reporter(Readable.from(events))) output += String(chunk); + console.log(`${name}:`, JSON.stringify(output)); +} + +await collect("spec", spec); +await collect("tap", tap); diff --git a/test-parity/node-suite/test/runner/api/run-empty.ts b/test-parity/node-suite/test/runner/api/run-empty.ts new file mode 100644 index 000000000..715fea059 --- /dev/null +++ b/test-parity/node-suite/test/runner/api/run-empty.ts @@ -0,0 +1,8 @@ +import { run } from "node:test"; + +console.log("run:before"); +let events = 0; +for await (const _event of run({ files: [] })) { + events++; +} +console.log("run:after", events); diff --git a/test-parity/node-suite/test/runner/context/assertion-methods.ts b/test-parity/node-suite/test/runner/context/assertion-methods.ts new file mode 100644 index 000000000..190527eea --- /dev/null +++ b/test-parity/node-suite/test/runner/context/assertion-methods.ts @@ -0,0 +1,11 @@ +import test from "node:test"; + +test("assertion methods", (t) => { + t.assert.ok(true); + t.assert.deepStrictEqual({ value: [1, 2] }, { value: [1, 2] }); + t.assert.match("node:test", /^node:/); + t.assert.throws(() => { + throw new TypeError("controlled assertion error"); + }, TypeError); + console.log("assertion methods:passed"); +}); diff --git a/test-parity/node-suite/test/runner/context/assertion-surface.ts b/test-parity/node-suite/test/runner/context/assertion-surface.ts new file mode 100644 index 000000000..2bd0f27ac --- /dev/null +++ b/test-parity/node-suite/test/runner/context/assertion-surface.ts @@ -0,0 +1,27 @@ +import test from "node:test"; + +test("assertion surface", (t) => { + const names = [ + "deepEqual", + "deepStrictEqual", + "doesNotMatch", + "doesNotReject", + "doesNotThrow", + "equal", + "fail", + "ifError", + "match", + "notDeepEqual", + "notStrictEqual", + "ok", + "rejects", + "strictEqual", + "throws", + "fileSnapshot", + "snapshot", + ]; + console.log( + "assertion surface:", + names.map((name) => `${name}:${typeof (t.assert as any)[name]}`).join(","), + ); +}); diff --git a/test-parity/node-suite/test/runner/context/assertions.ts b/test-parity/node-suite/test/runner/context/assertions.ts new file mode 100644 index 000000000..95fbd401b --- /dev/null +++ b/test-parity/node-suite/test/runner/context/assertions.ts @@ -0,0 +1,12 @@ +import test from "node:test"; + +test("context assertions", (t) => { + console.log( + "assertions:", + ["ok", "strictEqual", "deepStrictEqual", "throws"] + .map((name) => `${name}:${typeof (t.assert as any)[name]}`) + .join(","), + ); + t.assert.strictEqual(2 + 2, 4); + console.log("assertions:passed"); +}); diff --git a/test-parity/node-suite/test/runner/context/diagnostics-multiple.ts b/test-parity/node-suite/test/runner/context/diagnostics-multiple.ts new file mode 100644 index 000000000..6fb784996 --- /dev/null +++ b/test-parity/node-suite/test/runner/context/diagnostics-multiple.ts @@ -0,0 +1,8 @@ +import test from "node:test"; + +test("multiple diagnostics", (t) => { + console.log("diagnostic:before"); + t.diagnostic("first message"); + t.diagnostic("second message"); + console.log("diagnostic:after"); +}); diff --git a/test-parity/node-suite/test/runner/context/failure-propagation.ts b/test-parity/node-suite/test/runner/context/failure-propagation.ts new file mode 100644 index 000000000..d03c88feb --- /dev/null +++ b/test-parity/node-suite/test/runner/context/failure-propagation.ts @@ -0,0 +1,10 @@ +import test from "node:test"; + +test("parent observes child failure", async (t) => { + console.log("parent:before-failure"); + await t.test("failing child", () => { + console.log("child:throwing"); + throw new Error("controlled child failure"); + }); + console.log("parent:after-failure"); +}); diff --git a/test-parity/node-suite/test/runner/context/local-mock-restoration.ts b/test-parity/node-suite/test/runner/context/local-mock-restoration.ts new file mode 100644 index 000000000..20608cba6 --- /dev/null +++ b/test-parity/node-suite/test/runner/context/local-mock-restoration.ts @@ -0,0 +1,17 @@ +import test from "node:test"; + +const target = { + value: 1, + read() { + return this.value; + }, +}; + +test("installs local mock", (t) => { + t.mock.method(target, "read", () => 99); + console.log("local-mock:during", target.read()); +}); + +test("local mock restored", () => { + console.log("local-mock:after", target.read(), (target.read as any).mock === undefined); +}); diff --git a/test-parity/node-suite/test/runner/context/metadata-values.ts b/test-parity/node-suite/test/runner/context/metadata-values.ts new file mode 100644 index 000000000..6ffdcc0a4 --- /dev/null +++ b/test-parity/node-suite/test/runner/context/metadata-values.ts @@ -0,0 +1,8 @@ +import test from "node:test"; + +test("named parent", async (t) => { + console.log("context name:", t.name); + await t.test("named child", (child) => { + console.log("child context name:", child.name); + }); +}); diff --git a/test-parity/node-suite/test/runner/context/only-registration.ts b/test-parity/node-suite/test/runner/context/only-registration.ts new file mode 100644 index 000000000..ade83d56d --- /dev/null +++ b/test-parity/node-suite/test/runner/context/only-registration.ts @@ -0,0 +1,9 @@ +import test from "node:test"; + +test("regular sibling", () => { + console.log("only:regular"); +}); + +test.only("marked only", () => { + console.log("only:marked"); +}); diff --git a/test-parity/node-suite/test/runner/context/plan-match.ts b/test-parity/node-suite/test/runner/context/plan-match.ts new file mode 100644 index 000000000..7e63b4445 --- /dev/null +++ b/test-parity/node-suite/test/runner/context/plan-match.ts @@ -0,0 +1,10 @@ +import test from "node:test"; + +test("matched assertion plan", (t) => { + t.plan(1); + t.assert.fileSnapshot( + { ok: true }, + "test-parity/node-suite/test/snapshots/file-object.json", + ); + console.log("plan:matched-one"); +}); diff --git a/test-parity/node-suite/test/runner/context/plan-mismatch.ts b/test-parity/node-suite/test/runner/context/plan-mismatch.ts new file mode 100644 index 000000000..446a829fb --- /dev/null +++ b/test-parity/node-suite/test/runner/context/plan-mismatch.ts @@ -0,0 +1,6 @@ +import test from "node:test"; + +test("unmet assertion plan", (t) => { + t.plan(1); + console.log("plan:expected-one"); +}); diff --git a/test-parity/node-suite/test/runner/context/plan-overrun.ts b/test-parity/node-suite/test/runner/context/plan-overrun.ts new file mode 100644 index 000000000..07d5320ba --- /dev/null +++ b/test-parity/node-suite/test/runner/context/plan-overrun.ts @@ -0,0 +1,11 @@ +import test from "node:test"; + +test("exceeded assertion plan", (t) => { + t.plan(0); + console.log("plan:before-extra"); + t.assert.fileSnapshot( + { ok: true }, + "test-parity/node-suite/test/snapshots/file-object.json", + ); + console.log("plan:after-extra"); +}); diff --git a/test-parity/node-suite/test/runner/context/plan-subtest.ts b/test-parity/node-suite/test/runner/context/plan-subtest.ts new file mode 100644 index 000000000..b9db19bdf --- /dev/null +++ b/test-parity/node-suite/test/runner/context/plan-subtest.ts @@ -0,0 +1,9 @@ +import test from "node:test"; + +test("subtest satisfies plan", async (t) => { + t.plan(1); + await t.test("planned child", () => { + console.log("plan-child:body"); + }); + console.log("plan-parent:complete"); +}); diff --git a/test-parity/node-suite/test/runner/context/plan-zero.ts b/test-parity/node-suite/test/runner/context/plan-zero.ts new file mode 100644 index 000000000..00f51c238 --- /dev/null +++ b/test-parity/node-suite/test/runner/context/plan-zero.ts @@ -0,0 +1,6 @@ +import test from "node:test"; + +test("zero assertion plan", (t) => { + t.plan(0); + console.log("plan:zero"); +}); diff --git a/test-parity/node-suite/test/runner/context/runtime-directives.ts b/test-parity/node-suite/test/runner/context/runtime-directives.ts new file mode 100644 index 000000000..02d9c9cf5 --- /dev/null +++ b/test-parity/node-suite/test/runner/context/runtime-directives.ts @@ -0,0 +1,13 @@ +import test from "node:test"; + +test("runtime skip", (t) => { + console.log("runtime-skip:before"); + t.skip("not applicable"); + console.log("runtime-skip:after"); +}); + +test("runtime todo", (t) => { + console.log("runtime-todo:before"); + t.todo("not implemented"); + console.log("runtime-todo:after"); +}); diff --git a/test-parity/node-suite/test/runner/context/subtest-directives.ts b/test-parity/node-suite/test/runner/context/subtest-directives.ts new file mode 100644 index 000000000..58415ea2c --- /dev/null +++ b/test-parity/node-suite/test/runner/context/subtest-directives.ts @@ -0,0 +1,11 @@ +import test from "node:test"; + +test("directive parent", async (t) => { + await t.test("option skip child", { skip: "option reason" }, () => { + console.log("skip-child:should-not-run"); + }); + await t.test("option todo child", { todo: "todo reason" }, () => { + console.log("todo-child:ran"); + }); + console.log("directive-parent:complete"); +}); diff --git a/test-parity/node-suite/test/runner/hooks/after-each-after-throw.ts b/test-parity/node-suite/test/runner/hooks/after-each-after-throw.ts new file mode 100644 index 000000000..1dc77d8c3 --- /dev/null +++ b/test-parity/node-suite/test/runner/hooks/after-each-after-throw.ts @@ -0,0 +1,10 @@ +import { afterEach, test } from "node:test"; + +afterEach(() => { + console.log("hook:afterEach-cleanup"); +}); + +test("body throws", () => { + console.log("body:before-throw"); + throw new Error("controlled body failure"); +}); diff --git a/test-parity/node-suite/test/runner/hooks/before-throw-cleanup.ts b/test-parity/node-suite/test/runner/hooks/before-throw-cleanup.ts new file mode 100644 index 000000000..cd5a43035 --- /dev/null +++ b/test-parity/node-suite/test/runner/hooks/before-throw-cleanup.ts @@ -0,0 +1,14 @@ +import { after, before, test } from "node:test"; + +before(() => { + console.log("hook:before-throw"); + throw new Error("controlled before failure"); +}); + +after(() => { + console.log("hook:after-cleanup"); +}); + +test("blocked body", () => { + console.log("body:should-not-run"); +}); diff --git a/test-parity/node-suite/test/runner/hooks/cleanup-after-throw.ts b/test-parity/node-suite/test/runner/hooks/cleanup-after-throw.ts new file mode 100644 index 000000000..7a53f2f2a --- /dev/null +++ b/test-parity/node-suite/test/runner/hooks/cleanup-after-throw.ts @@ -0,0 +1,10 @@ +import { after, test } from "node:test"; + +after(() => { + console.log("cleanup:after"); +}); + +test("throws", () => { + console.log("body:before-throw"); + throw new Error("controlled hook cleanup failure"); +}); diff --git a/test-parity/node-suite/test/runner/hooks/context-name.ts b/test-parity/node-suite/test/runner/hooks/context-name.ts new file mode 100644 index 000000000..16d64f4cb --- /dev/null +++ b/test-parity/node-suite/test/runner/hooks/context-name.ts @@ -0,0 +1,6 @@ +import { after, before, test } from "node:test"; + +before((t) => console.log("before context:", t.name)); +after((t) => console.log("after context:", t.name)); + +test("hook context body", () => console.log("hook-context:body")); diff --git a/test-parity/node-suite/test/runner/hooks/multiple-order.ts b/test-parity/node-suite/test/runner/hooks/multiple-order.ts new file mode 100644 index 000000000..096a0d647 --- /dev/null +++ b/test-parity/node-suite/test/runner/hooks/multiple-order.ts @@ -0,0 +1,8 @@ +import { after, before, test } from "node:test"; + +before(() => console.log("before:first")); +before(() => console.log("before:second")); +after(() => console.log("after:first")); +after(() => console.log("after:second")); + +test("ordered hooks", () => console.log("hooks:body")); diff --git a/test-parity/node-suite/test/runner/hooks/nested-before-each-throw.ts b/test-parity/node-suite/test/runner/hooks/nested-before-each-throw.ts new file mode 100644 index 000000000..1312fa485 --- /dev/null +++ b/test-parity/node-suite/test/runner/hooks/nested-before-each-throw.ts @@ -0,0 +1,18 @@ +import { after, afterEach, beforeEach, describe, test } from "node:test"; + +describe("nested hook failure", () => { + beforeEach(() => { + console.log("nested:beforeEach-throw"); + throw new Error("controlled beforeEach failure"); + }); + afterEach(() => { + console.log("nested:afterEach-cleanup"); + }); + after(() => { + console.log("nested:after-cleanup"); + }); + + test("blocked child", () => { + console.log("body:should-not-run"); + }); +}); diff --git a/test-parity/node-suite/test/runner/hooks/nested-suites.ts b/test-parity/node-suite/test/runner/hooks/nested-suites.ts new file mode 100644 index 000000000..df3dfec20 --- /dev/null +++ b/test-parity/node-suite/test/runner/hooks/nested-suites.ts @@ -0,0 +1,15 @@ +import { after, afterEach, before, beforeEach, describe, it } from "node:test"; + +before(() => console.log("outer:before")); +after(() => console.log("outer:after")); +beforeEach(() => console.log("outer:beforeEach")); +afterEach(() => console.log("outer:afterEach")); + +describe("nested hooks", () => { + before(() => console.log("inner:before")); + after(() => console.log("inner:after")); + beforeEach(() => console.log("inner:beforeEach")); + afterEach(() => console.log("inner:afterEach")); + + it("child", () => console.log("body:child")); +}); diff --git a/test-parity/node-suite/test/runner/hooks/runtime-skip-after-each.ts b/test-parity/node-suite/test/runner/hooks/runtime-skip-after-each.ts new file mode 100644 index 000000000..7ff117c06 --- /dev/null +++ b/test-parity/node-suite/test/runner/hooks/runtime-skip-after-each.ts @@ -0,0 +1,9 @@ +import { afterEach, beforeEach, test } from "node:test"; + +beforeEach(() => console.log("skip:beforeEach")); +afterEach(() => console.log("skip:afterEach")); + +test("runtime skip cleanup", (t) => { + console.log("skip:body"); + t.skip("controlled runtime skip"); +}); diff --git a/test-parity/node-suite/test/runner/hooks/top-level-order.ts b/test-parity/node-suite/test/runner/hooks/top-level-order.ts new file mode 100644 index 000000000..04558cbba --- /dev/null +++ b/test-parity/node-suite/test/runner/hooks/top-level-order.ts @@ -0,0 +1,9 @@ +import { after, afterEach, before, beforeEach, test } from "node:test"; + +before(() => console.log("hook:before")); +after(() => console.log("hook:after")); +beforeEach(() => console.log("hook:beforeEach")); +afterEach(() => console.log("hook:afterEach")); + +test("first", () => console.log("body:first")); +test("second", () => console.log("body:second")); diff --git a/test-parity/node-suite/test/runner/registration/async-callback.ts b/test-parity/node-suite/test/runner/registration/async-callback.ts new file mode 100644 index 000000000..81a6a8192 --- /dev/null +++ b/test-parity/node-suite/test/runner/registration/async-callback.ts @@ -0,0 +1,8 @@ +import test from "node:test"; + +test("async callback", async () => { + console.log("async:start"); + await Promise.resolve(); + console.log("async:after-await"); + return "ignored return value"; +}); diff --git a/test-parity/node-suite/test/runner/registration/callback-overloads.ts b/test-parity/node-suite/test/runner/registration/callback-overloads.ts new file mode 100644 index 000000000..04d34e0d5 --- /dev/null +++ b/test-parity/node-suite/test/runner/registration/callback-overloads.ts @@ -0,0 +1,17 @@ +import test from "node:test"; + +test(() => { + console.log("overload:callback-only"); +}); + +test(function namedCallback() { + console.log("overload:named-callback"); +}); + +test({ skip: false }, () => { + console.log("overload:options-callback"); +}); + +test("name-options-callback", { skip: false }, (t) => { + console.log("overload:full", t.name); +}); diff --git a/test-parity/node-suite/test/runner/registration/done-callback.ts b/test-parity/node-suite/test/runner/registration/done-callback.ts new file mode 100644 index 000000000..d1b1f3940 --- /dev/null +++ b/test-parity/node-suite/test/runner/registration/done-callback.ts @@ -0,0 +1,7 @@ +import test from "node:test"; + +test("callback completion", (_t, done) => { + console.log("done:body"); + done(); + console.log("done:after-callback"); +}); diff --git a/test-parity/node-suite/test/runner/registration/missing-callback.ts b/test-parity/node-suite/test/runner/registration/missing-callback.ts new file mode 100644 index 000000000..2ecf5e438 --- /dev/null +++ b/test-parity/node-suite/test/runner/registration/missing-callback.ts @@ -0,0 +1,5 @@ +import test from "node:test"; + +console.log("missing:before"); +test("test without callback"); +console.log("missing:after"); diff --git a/test-parity/node-suite/test/runner/registration/nested-subtests.ts b/test-parity/node-suite/test/runner/registration/nested-subtests.ts new file mode 100644 index 000000000..1155380a8 --- /dev/null +++ b/test-parity/node-suite/test/runner/registration/nested-subtests.ts @@ -0,0 +1,9 @@ +import test from "node:test"; + +test("parent", async (t) => { + console.log("parent:body:start"); + await t.test("child", () => { + console.log("child:body"); + }); + console.log("parent:body:end"); +}); diff --git a/test-parity/node-suite/test/runner/registration/options-validation.ts b/test-parity/node-suite/test/runner/registration/options-validation.ts new file mode 100644 index 000000000..987082ff4 --- /dev/null +++ b/test-parity/node-suite/test/runner/registration/options-validation.ts @@ -0,0 +1,15 @@ +import test from "node:test"; + +function codeOf(fn: () => void): string { + try { + fn(); + return "NO_THROW"; + } catch (error) { + return (error as any).code ?? (error as Error).name; + } +} + +console.log("timeout string:", codeOf(() => test({ timeout: "1" as any }))); +console.log("timeout negative:", codeOf(() => test({ timeout: -1 }))); +console.log("concurrency string:", codeOf(() => test({ concurrency: "1" as any }))); +console.log("concurrency zero:", codeOf(() => test({ concurrency: 0 }))); diff --git a/test-parity/node-suite/test/runner/registration/parent-child-completion.ts b/test-parity/node-suite/test/runner/registration/parent-child-completion.ts new file mode 100644 index 000000000..994e3c82a --- /dev/null +++ b/test-parity/node-suite/test/runner/registration/parent-child-completion.ts @@ -0,0 +1,13 @@ +import test from "node:test"; + +test("parent completion", async (t) => { + console.log("parent:start"); + const child = t.test("async child", async () => { + console.log("child:start"); + await Promise.resolve(); + console.log("child:end"); + }); + console.log("parent:registered-child"); + await child; + console.log("parent:after-child"); +}); diff --git a/test-parity/node-suite/test/runner/registration/suite-nesting.ts b/test-parity/node-suite/test/runner/registration/suite-nesting.ts new file mode 100644 index 000000000..b982af37a --- /dev/null +++ b/test-parity/node-suite/test/runner/registration/suite-nesting.ts @@ -0,0 +1,10 @@ +import { describe, it } from "node:test"; + +describe("outer suite", () => { + console.log("suite:body"); + describe("inner suite", () => { + it("nested test", () => { + console.log("test:body"); + }); + }); +}); diff --git a/test-parity/node-suite/test/runner/registration/top-level-order.ts b/test-parity/node-suite/test/runner/registration/top-level-order.ts new file mode 100644 index 000000000..6c4b12289 --- /dev/null +++ b/test-parity/node-suite/test/runner/registration/top-level-order.ts @@ -0,0 +1,7 @@ +import test from "node:test"; + +console.log("registration:start"); +test("top level", () => { + console.log("body:top level"); +}); +console.log("registration:end"); diff --git a/test-parity/node-suite/test/snapshots/assertion-validation.ts b/test-parity/node-suite/test/snapshots/assertion-validation.ts new file mode 100644 index 000000000..a48bc6762 --- /dev/null +++ b/test-parity/node-suite/test/snapshots/assertion-validation.ts @@ -0,0 +1,19 @@ +import test from "node:test"; + +function codeOf(fn: () => void): string { + try { + fn(); + return "NO_THROW"; + } catch (error) { + return (error as any).code ?? (error as Error).name; + } +} + +test("snapshot assertion validation", (t) => { + console.log("snapshot options:", codeOf(() => (t.assert.snapshot as any)("value", null))); + console.log("file path:", codeOf(() => (t.assert.fileSnapshot as any)({}, 5))); + console.log( + "file options:", + codeOf(() => (t.assert.fileSnapshot as any)({}, "missing.snapshot", null)), + ); +}); diff --git a/test-parity/node-suite/test/snapshots/serializer-validation.ts b/test-parity/node-suite/test/snapshots/serializer-validation.ts new file mode 100644 index 000000000..311f36c4f --- /dev/null +++ b/test-parity/node-suite/test/snapshots/serializer-validation.ts @@ -0,0 +1,15 @@ +import { snapshot } from "node:test"; + +function codeOf(fn: () => void): string { + try { + fn(); + return "NO_THROW"; + } catch (error) { + return (error as any).code ?? (error as Error).name; + } +} + +console.log("not array:", codeOf(() => snapshot.setDefaultSnapshotSerializers("x" as any))); +console.log("bad entry:", codeOf(() => snapshot.setDefaultSnapshotSerializers([() => "ok", 1 as any]))); +console.log("valid:", codeOf(() => snapshot.setDefaultSnapshotSerializers([(value) => String(value)]))); +snapshot.setDefaultSnapshotSerializers([]); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 56a4fa4e8..01376fd24 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -170,8 +170,8 @@ "total": 2 }, "test": { - "pass": 7, - "total": 11 + "pass": 33, + "total": 90 }, "timers": { "pass": 90,