diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 5e368cc03..70143a0c3 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -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 ``. 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 ``, 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`. diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index bb525e02d..c31e674af 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -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 { diff --git a/tests/fixtures/node-test/cases/test-name-forms.ts b/tests/fixtures/node-test/cases/test-name-forms.ts index 915d68a79..b2f61dd89 100644 --- a/tests/fixtures/node-test/cases/test-name-forms.ts +++ b/tests/fixtures/node-test/cases/test-name-forms.ts @@ -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 "". With both skip and todo, skip wins, in either order and +// or "". 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);