A thrown array reached its catch binding as a type-erased native array:
`console.log(e)` printed `{}` where Node prints the array, `String(e)`
gave "[object Object]", `Array.isArray(e)` was false and a typed view
such as `(e as number[]).length` threw a TypeError. Thrown records
already crossed the catch boundary as checked-dynamic values that keep
their identity; arrays, and unions over records and arrays, now take the
same conversion, so the catch binding is the thrown object.
node:test printed a failing test's thrown non-Error value with String():
`throw { code: 42 }` reported `[object Object]` where Node prints the
value as util.inspect renders it. The failing-tests section now inspects
thrown numbers, booleans and checked-dynamic values (records, arrays,
null and undefined); a thrown string is quoted as before. A thrown class
instance that is not an Error still prints `[object Object]` (the
limitations page says so).
Adds corpus 6341, extends the thrown-values node:test fixture, and
records 6341's cost.
node:test changes, matching Node 24.22:
- mock.fn() and t.mock.fn() without an original return undefined and
record the arguments of each call, whatever the callers pass. The
original and the initial implementation are a function that returns
undefined, so restore() brings that function back.
- mock.fn<F>() with an explicit function type records F's parameters
when F's return type is void or includes undefined. A return type
without undefined, such as mock.fn<() => number>(), throws SC2020.
mock.fn()'s default type, (...args: any[]) => undefined, maps to the
checked-dynamic rest ABI used for program-declared (...args: unknown[])
functions: the calls log records the argument array, and the mock's
wrapper spreads it into the implementation.
A function whose fixed parameters are all unknown or any, or that has
none, now converts to a checked-dynamic rest signature without fixed
parameters, such as (...args: unknown[]) => void: each call passes the
arguments in order, undefined for the ones the caller left out. This is
how () => {} reaches mockImplementation and mockImplementationOnce, and
it applies to any such slot in TypeScript programs, which threw SC1090
before.
The mock-fn-default fixture and corpus program 5816 compare with Node,
plain and with SCRIPTC_SAN=1.
node:test changes, matching Node 24.22:
- getTestContext() returns the context of the test, suite or hook whose
code is running: undefined at module top level, the root's context in
module-level hooks, the suite's in a suite body and in the suite's
hooks, and the test's in its body. In the afterEach and t.after hooks
of a subtest cancelled by its parent's timeout, it returns each hook's
owner.
- The context rides the AsyncLocalStorage snapshot under a reserved id,
so it follows a test across its awaits, into the async functions,
promise callbacks, timers and other callbacks it schedules, and into
concurrent subtests, which each see their own.
- The root's fullName is "<root>", as its name is.
- Optional chains over a test context, such as getTestContext()?.name,
lower.
AsyncLocalStorage enter and restore move into the always-linked async
core so the test runner can use them. The snapshot holds the node's
index in a table of live nodes, so a snapshot that outlives its node
answers undefined.
The get-test-context and get-test-context-cancelled fixtures compare
the contexts seen at each point with Node, plain and with SCRIPTC_SAN=1.
A test name computed at run time took the function's name when it was
empty, which needs that name at compile time. For a function the
program received as a parameter, such as a helper that registers
`test(\`case ${label}\`, body)` for its callers, the call was refused,
where it compiled before. The name is now used as given in that case,
and the limitations page notes that an empty one stays empty, where
Node uses the function's name.
The test-name-forms fixture registers tests through such a helper and
compares them with Node, plain and with SCRIPTC_SAN=1. Without this
change it fails to compile.
node:test changes, matching Node 24.22:
- test, it, describe, suite, their skip/todo/only twins and t.test take
Node's (name?, options?, fn?) forms: test(fn), test(options, fn),
test(options) and test() compile, as does describe(fn) and
describe(options, fn). The fallback declarations add the overloads.
- A missing or empty name is the function's name, or "<anonymous>", as
Node's Test constructor completes it. The name is the function
expression's or declaration's own name, or the binding name of a
const arrow or anonymous function; a name that is empty at run time
takes the same default.
- With both skip and todo, the test is skipped, and the directive message
is the skip string, else the todo string. The skip and todo method
twins override the option of the same name, so test.skip("x",
{ skip: "msg" }) reports a bare SKIP and it.skip("x", { todo: "msg" })
reports "msg". Before, the last option set the mode.
The limitations page notes that a thrown object or array that is not an
Error reports [object Object] in the failing section.
The test-name-forms fixture compares the names, directives and counts
with Node, plain and with SCRIPTC_SAN=1.
node:test changes, matching Node 24.22:
- A subtest cancelled because its parent timed out has its mocks
restored and its mock timers reset when it is cancelled, as Node's
postRun resets the test's mock tracker. Before, a mocked method stayed
mocked for the tests that ran after it.
- Its afterEach chain and t.after hooks run at the next turn, once the
runner has moved on to the next test, as Node runs them when the
cancelled test's own run resumes. Before, they never ran. The chain is
copied when the test is cancelled, since its ancestors drop their hooks
when they finish.
- Its hooks are then dropped, so closures that capture its context do not
keep it alive. The registration parent is still not restored when the
cancelled body settles: it belongs to the test that runs then.
The timeout-cancel-cleanup fixture records the mocked method and the hook
order against the next test's body and compares them with Node, plain and
with SCRIPTC_SAN=1. It fails without this change.
node:test changes, matching Node 24.22:
- When a test's { timeout } elapses, its subtests that are still running
are cancelled, the innermost first. Each reports its duration so far and
fails with "test did not finish before its parent and was cancelled",
before the parent's result.
- A cancelled subtest's body keeps running unobserved: when it settles it
reports nothing and leaves the runner's state alone. Before, it reported
its result after the run's summary.
The timeout-subtests fixture compares the results, counts and failing
section with Node, plain and with SCRIPTC_SAN=1.
node:test changes, matching Node 24.22:
- before and after hooks outside any suite run, with the root's context,
whose name is "<root>". They did not run before.
- A module-level before hook runs at its before() call, as Node runs the
hooks of its already-started root at once: its synchronous part runs
before the code after the call, and the first top-level test waits for
an async hook's promise. Every module-level before hook runs, and an
async hook's rejection is not reported as an unhandled rejection.
- Module-level after hooks run after the last top-level test or suite.
- A throwing root before hook fails each top-level test with the first
failure; the test's body and beforeEach hooks do not run, and its
afterEach hooks do. Each top-level suite fails with the error, and the
tests and suites in it are cancelled before they start: they report
without a duration and fail with "test did not finish before its parent
and was cancelled", listed in the failing section in completion order.
- A throwing root after hook reports the entry file as a failed result,
after the top-level results, and lists it in the failing section at the
hook's location. It is not counted, and the exit code is unchanged.
- Hooks record the "file:line:col" of their call for that location.
The root-hooks, root-before-throws and root-after-throws fixtures compare
the reporter output, the order of the hooks against the module code, and
the exit codes with Node, plain and with SCRIPTC_SAN=1.
node:test changes, matching Node 24.22:
- A describe or suite body may take its SuiteContext, with name,
fullName, filePath, attempt, passed and diagnostic. Its passed is false
until the suite has finished, after its after hooks.
- before and after hooks may take the suite's context, and beforeEach
and afterEach hooks the test's. A test's t.before, t.after,
t.beforeEach and t.afterEach hooks may take the test's context.
- SuiteContext maps to the runner node that TestContext maps to, so the
TestContext members that SuiteContext shares lower the same way.
The suite-context fixture compares the reporter output and the order of
the bodies and hooks with Node, plain and with SCRIPTC_SAN=1.
node:test changes, matching Node 24.22:
- t.filePath is the entry file's path, as Node's entryFile, fixed when the
program is compiled.
- t.attempt is 0, since a test runs once.
- t.passed is false while the body runs and the test's outcome once the
body ends, so afterEach and t.after hooks see it, as Node's pass() and
fail() set it before them.
- t.workerId is Number(process.env.NODE_TEST_WORKER_ID) || undefined.
- t.tags is [], since the tags option has no lowering.
- A test's t.before, t.after, t.beforeEach and t.afterEach hooks are
released when the test ends. Hooks whose closures capture the test's
context no longer keep it alive in a cycle, which the SCRIPTC_SAN=1
audit reported as live closures at exit.
The ctx-members fixture compares the reporter output with Node, plain and
with SCRIPTC_SAN=1.
node:test changes, matching Node 24.22:
- t.assert.snapshot(value[, options]) serializes the value with Node's
default serializer, JSON.stringify(value, null, 2), or with an
options.serializers array literal applied in order, and compares it
with assert.strictEqual against the snapshot stored under
"<test full name> <n>" in "<entry file>.snapshot".
- t.assert.fileSnapshot(value, path[, options]) compares the serialized
value with the contents of path.
- Snapshot files are read in the form Node writes them, exports[`key`] =
`value`; statements, with template escapes and CRLF line endings
cooked as in a template literal.
- Missing snapshots, unreadable snapshot files and throwing serializers
throw Node's ERR_INVALID_STATE errors, with Node's messages.
- Running the binary with Node's --test-update-snapshots flag as its
first argument records the values and writes the snapshot files after
the last test, keys sorted as Node sorts them. The flag is kept out of
process.argv and appears in process.execArgv, which is otherwise [].
- Snapshot assertions count toward the test's plan.
The snapshot fixture compares the reporter output with Node, plain and
with SCRIPTC_SAN=1, against a snapshot file Node wrote. The update-mode
harness test runs Node and the binary with --test-update-snapshots and
compares the files they write. The node:test normalization also drops
the "... N lines matching cause stack trace ..." line Node prints for an
error cause, with the stack frames it summarizes.
Node sets a test's concurrency to Infinity for `true` whenever the test
has a parent, and to os.availableParallelism() - 1 only for the root
test. Every test and suite a program declares has a parent, so `true`
now runs all of its subtests at once instead of capping them at the CPU
count minus one.
The concurrency fixture adds a suite of 64 concurrent tests that checks
all of them run at once.
node:test changes, matching Node 24.22:
- { concurrency } on describe, suite, test, it and t.test sets how many
subtests run at once: a number, true for os.availableParallelism() - 1,
or false for 1. Subtests inherit the parent's value.
- Above 1, each subtest runs on its own fiber. Subtests start in
declaration order as slots free up, and t.test and test() calls inside a
concurrent test start the subtest at once; t.test's promise settles when
it finishes. The parent finishes after all of them.
- Results, diagnostics and failures are reported in declaration order.
- A concurrent subtest waits after its beforeEach hooks, and a concurrent
suite before its children, for as many turns as Node's runner does, so
sibling bodies interleave as they do there.
- A concurrency that is not an integer from 1 to 4294967295 throws Node's
ERR_OUT_OF_RANGE RangeError when the test or suite is created.
- Nested test() calls and hooks inside a concurrent subtest attach to the
subtest that runs on the calling fiber.
The concurrency fixture compares the reporter output, the order of the
bodies' side effects, and the range errors with Node, plain and with
SCRIPTC_SAN=1.
node:test's mock.fn(original[, implementation]) and t.mock.fn compile.
A Mock<F> is the function-with-properties record { %call, mock }: the
callable is a wrapper closure that shares the mock context with the mock
field. The context records each call's arguments, result, error and this,
and keeps the current implementation, the original and the one-time
implementations by call index. fn.mock supports, as in Node 24.22:
- calls, read as a copy of the log;
- callCount();
- mockImplementation(fn);
- mockImplementationOnce(fn[, onCall]), for the next call by default, a
later registration for the same index replacing the earlier one;
- resetCalls();
- restore(), which brings back the original.
A throwing implementation records the error and rethrows it. Functions
with fixed parameters or a single rest array parameter are supported; a
union return type needs an undefined arm. mock.fn() without an original,
mock.method, the tracker's reset and restoreAll, and t.mock.method are
refused with SC2020.
The node:test fixture mock-fn compares the behavior with Node, and
tests/diagnostics/mock-fences.ts covers the refusals.
node:test changes, matching Node 24.22:
- t.before, t.after, t.beforeEach and t.afterEach lower. t.before runs
when it is called; a throw fails the test and the subtests that follow,
which skip their body and beforeEach hooks and run their afterEach hooks.
t.beforeEach and t.afterEach wrap every descendant subtest. t.after runs
after the test's afterEach chain, also when the body throws, and a throw
fails the test.
- t.plan(count) counts the test's t.assert calls and subtests. A mismatch
fails the test with Node's "plan expected N assertions but received M"
string, unless an assertion failed first.
- t.fullName joins the names from the outermost suite or test with " > ".
- Tests and suites under a todo suite or test are todo, with a bare TODO
directive, unless they are skipped or carry their own todo message. A
todo suite reports its directive.
- describe.only, suite.only and { only: true } on a suite mark it. At each
level, when a child is marked or contains a marked test or suite, only
those children run.
- The failing-tests section lists failures in completion order, so
subtests come before the test that ran them, and prints thrown strings
quoted, as Node inspects them.
The fixtures ctx-hooks, ctx-plan, todo-suites, suite-variants,
only-nested and thrown-values compare the reporter output and exit code
with Node.
- The ratified rename rider (world-unification §7 lane 5): "DOM" collided with the browser DOM and, post-lane-1, the structure is no longer a document tree. Comments and doc prose now say "the checked-dynamic tree" for the noun and "dyn" adjectivally; runtime symbol names (scr_dyn_*), the error.domClone libCall, and the real web DOMException family are unchanged.
- The few user-facing strings carrying the nickname move with it (the structuredClone cyclic message, the dc subscriber/publish fences, two emitter ICE texts); diagnostics snapshots regenerated (96 pass). One private compiler constant (DOM_PROTO_METHOD_NAMES -> DYN_PROTO_METHOD_NAMES) rides along.
- Mechanical sweep, 142 files, comments/prose only — no behavior change.