Use a computed test name as given when the function's name is unknown

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.
This commit is contained in:
Chris Tate
2026-10-19 03:22:05 -05:00
parent 60535fd09c
commit f75b7d8aab
3 changed files with 17 additions and 1 deletions
@@ -5,3 +5,4 @@
- **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test. Both get the root's context, named `<root>`. As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite. A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged.
- **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started.
- **Thrown values:** a test that throws an object or array that is not an `Error`, such as `throw { code: 42 }`, reports `[object Object]` in the failing section, where Node prints the value as `util.inspect` renders it.
- **Test names:** a test without a name, or with an empty one, takes its function's name or `<anonymous>`, as in Node, when the function's name is known at compile time: a function expression, a function declaration, or a `const` bound to an arrow or anonymous function. A name computed at run time, with a function the program received as a parameter, is used as given, so an empty one stays empty where Node uses the function's name. `test(fn)` and `test("", fn)` with such a function throw `SC2020`.
@@ -409,6 +409,12 @@ function lowerTestName(
if (!nameNode) return strLit(fallback(), loc);
const name = lowerer.lowerExprExpecting(nameNode, STRING);
if (name.kind === "strLit") return name.value === "" ? strLit(fallback(), loc) : name;
// A name computed at run time: when the function's own name is not known
// at compile time (a function passed in as a parameter, say), the name
// is used as given, and an empty one stays empty (the limitations page
// notes it). Refusing here would refuse every helper that registers
// tests for its callers.
if (fnNode && staticFunctionName(lowerer, fnNode) === null) return name;
const slot = lowerer.declareHiddenLocal("%testName", STRING);
const value = varRef(slot.id, STRING, loc);
return {
+10 -1
View File
@@ -1,6 +1,7 @@
// The (name?, options?, fn?) forms of test, it, describe and t.test, and
// Node's default names: a missing or empty name takes the function's name,
// or "<anonymous>". With both skip and todo, skip wins, in either order and
// or "<anonymous>". A helper may register tests with computed names and
// functions it receives as parameters. With both skip and todo, skip wins, in either order and
// against the todo method twins.
import { describe, it, test } from "node:test";
@@ -8,6 +9,14 @@ function declaredTest(): void {}
const boundArrow = (): void => {};
const emptyName = process.argv.length > 99 ? "never" : "";
// A helper that registers tests for its callers: the name is computed and
// the function arrives as a parameter.
function registerCase(label: string, body: () => void): void {
test(`case ${label}`, body);
}
registerCase("one", () => {});
registerCase("two", declaredTest);
test(function namedExpression() {});
test(() => {});
test(declaredTest);