From 60535fd09ceb77ce992e9cf78362b8d5afaf52f9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 00:21:03 -0500 Subject: [PATCH 1/2] Accept test and describe calls without a name, and settle skip and todo as Node does 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 "", 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. --- .../limitations/builtin-apis/test-runner.mdx | 1 + .../ambient/scriptc-node-fallback.d.ts | 53 +++-- .../src/frontend/lowering/lower-test.ts | 218 ++++++++++++++---- .../node-test/cases/test-name-forms.ts | 46 ++++ 4 files changed, 256 insertions(+), 62 deletions(-) create mode 100644 tests/fixtures/node-test/cases/test-name-forms.ts diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 77d3d9a84..5e368cc03 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -4,3 +4,4 @@ - **Suite and hook contexts:** a `describe` body may take its `SuiteContext`, which has `name`, `fullName`, `filePath`, `attempt`, `passed` and `diagnostic`. Hooks may take their context, as in Node: the suite's for `before` and `after`, the test's for `beforeEach`, `afterEach`, `t.before` and `t.after`. The `SuiteContext` `signal` and `log` members, and hook `done` callbacks, throw `SC2020`. - **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. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 9655b7580..3e3f66a2a 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3405,8 +3405,10 @@ declare module "node:test" { readonly tags: readonly string[]; readonly assert: TestContextAssert; readonly mock: MockTracker; - test(name: string, fn?: TestFn): Promise; - test(name: string, options: TestOptions, fn?: TestFn): Promise; + test(name?: string, fn?: TestFn): Promise; + test(name?: string, options?: TestOptions, fn?: TestFn): Promise; + test(options?: TestOptions, fn?: TestFn): Promise; + test(fn?: TestFn): Promise; skip(message?: string): void; todo(message?: string): void; diagnostic(message: string): void; @@ -3436,23 +3438,44 @@ declare module "node:test" { match(value: string, regExp: RegExp, message?: string): void; doesNotMatch(value: string, regExp: RegExp, message?: string): void; } - function test(name: string, fn?: TestFn): Promise; - function test(name: string, options: TestOptions, fn?: TestFn): Promise; + /* The name, options and function are each optional, as in Node: a test + * without a name takes the function's name, or "". */ + function test(name?: string, fn?: TestFn): Promise; + function test(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function test(options?: TestOptions, fn?: TestFn): Promise; + function test(fn?: TestFn): Promise; namespace test { - function skip(name: string, fn?: TestFn): Promise; - function skip(name: string, options: TestOptions, fn?: TestFn): Promise; - function todo(name: string, fn?: TestFn): Promise; - function todo(name: string, options: TestOptions, fn?: TestFn): Promise; - function only(name: string, fn?: TestFn): Promise; - function only(name: string, options: TestOptions, fn?: TestFn): Promise; + function skip(name?: string, fn?: TestFn): Promise; + function skip(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function skip(options?: TestOptions, fn?: TestFn): Promise; + function skip(fn?: TestFn): Promise; + function todo(name?: string, fn?: TestFn): Promise; + function todo(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function todo(options?: TestOptions, fn?: TestFn): Promise; + function todo(fn?: TestFn): Promise; + function only(name?: string, fn?: TestFn): Promise; + function only(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function only(options?: TestOptions, fn?: TestFn): Promise; + function only(fn?: TestFn): Promise; } const it: typeof test; - function describe(name: string, fn?: SuiteFn): void; - function describe(name: string, options: TestOptions, fn?: SuiteFn): void; + function describe(name?: string, fn?: SuiteFn): void; + function describe(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function describe(options?: TestOptions, fn?: SuiteFn): void; + function describe(fn?: SuiteFn): void; namespace describe { - function skip(name: string, fn?: SuiteFn): void; - function todo(name: string, fn?: SuiteFn): void; - function only(name: string, fn?: SuiteFn): void; + function skip(name?: string, fn?: SuiteFn): void; + function skip(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function skip(options?: TestOptions, fn?: SuiteFn): void; + function skip(fn?: SuiteFn): void; + function todo(name?: string, fn?: SuiteFn): void; + function todo(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function todo(options?: TestOptions, fn?: SuiteFn): void; + function todo(fn?: SuiteFn): void; + function only(name?: string, fn?: SuiteFn): void; + function only(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function only(options?: TestOptions, fn?: SuiteFn): void; + function only(fn?: SuiteFn): void; } const suite: typeof describe; function before(fn: HookFn): void; diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index e36518559..bb525e02d 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -90,12 +90,14 @@ function atStringOf(expr: ts.CallExpression): string { return `${sf.fileName}:${pos.line + 1}:${pos.character + 1}`; } -/** One parsed { skip?, todo?, only? } options literal. `mode` is the - * runtime literal (0 run / 1 skip / 2 todo), `msg` the directive message - * expression ("" = none). */ +/** One parsed { skip?, todo?, only? } options literal. `skip` and `todo` + * are whether each is set (a string or true), and `skipMsg` and `todoMsg` + * the string messages (directiveOf settles them). */ interface TestOptions { - mode: number; - msg: IrExpr | null; + skip: boolean; + skipMsg: IrExpr | null; + todo: boolean; + todoMsg: IrExpr | null; only: boolean; /** The { plan } count, -1 without one. */ plan: number; @@ -109,8 +111,10 @@ interface TestOptions { } const noOptions = (): TestOptions => ({ - mode: 0, - msg: null, + skip: false, + skipMsg: null, + todo: false, + todoMsg: null, only: false, plan: -1, timeout: null, @@ -118,6 +122,23 @@ const noOptions = (): TestOptions => ({ pre: [], }); +/** The runtime mode (0 run / 1 skip / 2 todo) and directive message of a + * test or suite, as Node's Test constructor settles them: the skip and todo + * method twins override the options of the same name, skip wins over + * todo, and the message is the skip string, else the todo string. */ +function directiveOf(opts: TestOptions, methodMode: number, loc: SrcLoc): [IrExpr, IrExpr] { + if (methodMode === 1) { + opts.skip = true; + opts.skipMsg = null; + } + if (methodMode === 2) { + opts.todo = true; + opts.todoMsg = null; + } + const mode = opts.skip ? 1 : opts.todo ? 2 : 0; + return [numLit(mode, loc), opts.skipMsg ?? opts.todoMsg ?? strLit("", loc)]; +} + /** Flags 8 and 16: a { timeout } and a { concurrency } are given. */ const optionFlags = (opts: TestOptions): number => (opts.only ? 4 : 0) | (opts.timeout ? 8 : 0) | (opts.concurrency ? 16 : 0); @@ -168,15 +189,33 @@ function lowerTestOptions( // A shorthand entry's value is the binding of the same name. const init: ts.Expression = ts.isPropertyAssignment(prop) ? prop.initializer : key; if (name === "skip" || name === "todo") { - const mode = name === "skip" ? 1 : 2; const t = lowerer.typeOf(init); if (t.isStringLiteralType() || lowerer.mapTypeOf(t)?.kind === "string") { - out.mode = mode; - out.msg = optionValue(lowerer, out, lowerer.lowerExprExpecting(init, STRING), locOf(init)); - } else if (init.kind === ts.SyntaxKind.TrueKeyword) { - out.mode = mode; - } else if (init.kind === ts.SyntaxKind.FalseKeyword) { - // explicit false: run normally + const msg = optionValue( + lowerer, + out, + lowerer.lowerExprExpecting(init, STRING), + locOf(init), + ); + if (name === "skip") { + out.skip = true; + out.skipMsg = msg; + } else { + out.todo = true; + out.todoMsg = msg; + } + } else if ( + init.kind === ts.SyntaxKind.TrueKeyword || + init.kind === ts.SyntaxKind.FalseKeyword + ) { + const on = init.kind === ts.SyntaxKind.TrueKeyword; + if (name === "skip") { + out.skip = on; + out.skipMsg = null; + } else { + out.todo = on; + out.todoMsg = null; + } } else { lowerer.noLowering( `${what} with a non-literal ${name} value`, @@ -284,6 +323,110 @@ function lowerBodyArg( /** The shared registration lowering behind test/it and their skip/todo/ * only method twins — and, with `sub`, behind t.test. Argument shapes: * (name), (name, fn), (name, options, fn). */ +/** The `name` a function value has in JavaScript, when the compiler can + * tell: "" for an anonymous function expression, the declared or binding + * name otherwise; null for other expressions. */ +function staticFunctionName(lowerer: Lowerer, node: ts.Expression): string | null { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (ts.isArrowFunction(node)) return ""; + if (ts.isFunctionExpression(node)) return node.name?.text ?? ""; + if (!ts.isIdentifier(node)) return null; + const symbol = lowerer.resolveValueSymbol(node); + if (!symbol) return null; + for (const decl of lowerer.checker.declarationsOf(symbol)) { + if (ts.isFunctionDeclaration(decl)) return decl.name?.text ?? "default"; + if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name) && decl.initializer) { + let init = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isFunctionExpression(init) && init.name) return init.name.text; + if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) return decl.name.text; + } + } + return null; +} + +/** Node's (name?, options?, fn?) argument forms (`Test#createSubtest`): a + * leading function is the body, a leading object the options, otherwise + * the first argument is the name and a following object the options. */ +function splitTestArgs( + lowerer: Lowerer, + expr: ts.CallExpression, + what: string, +): { nameNode?: ts.Expression; optionsNode?: ts.Expression; fnNode?: ts.Expression } { + const args = expr.arguments; + const isFn = (a: ts.Expression): boolean => + lowerer.checker.getCallSignatures(lowerer.typeOf(a)).length > 0; + const isOptions = (a: ts.Expression): boolean => ts.isObjectLiteralExpression(a); + const unsupported = (): never => + lowerer.noLowering( + `${what} with these arguments`, + expr, + "the supported forms are (name?, options?, fn?) with a string name, an options object literal, and a function", + ); + if (args.length > 3 || args.some(ts.isSpreadElement)) unsupported(); + const out: { nameNode?: ts.Expression; optionsNode?: ts.Expression; fnNode?: ts.Expression } = {}; + let i = 0; + const next = args[i]; + if (next !== undefined && !isFn(next) && !isOptions(next)) { + out.nameNode = next; + i++; + } + const options = args[i]; + if (options !== undefined && isOptions(options)) { + out.optionsNode = options; + i++; + } + const fn = args[i]; + if (fn !== undefined && isFn(fn)) { + out.fnNode = fn; + i++; + } + if (i < args.length) unsupported(); + return out; +} + +/** The test's name, as Node's Test constructor completes it: a missing or + * empty name takes the function's name, or "". */ +function lowerTestName( + lowerer: Lowerer, + expr: ts.CallExpression, + what: string, + nameNode: ts.Expression | undefined, + fnNode: ts.Expression | undefined, + loc: SrcLoc, +): IrExpr { + const fallback = (): string => { + const fnName = fnNode ? staticFunctionName(lowerer, fnNode) : ""; + if (fnName === null) { + lowerer.noLowering( + `${what} without a name, with a function whose name is not known at compile time`, + fnNode ?? expr, + "give the test a name, or pass a function expression or a declared function", + ); + } + return fnName || ""; + }; + if (!nameNode) return strLit(fallback(), loc); + const name = lowerer.lowerExprExpecting(nameNode, STRING); + if (name.kind === "strLit") return name.value === "" ? strLit(fallback(), loc) : name; + const slot = lowerer.declareHiddenLocal("%testName", STRING); + const value = varRef(slot.id, STRING, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: slot.id, init: name, loc }], + result: { + kind: "ternary", + cond: { kind: "strEq", negated: false, left: value, right: strLit("", loc), type: BOOL, loc }, + then: strLit(fallback(), loc), + else_: value, + type: STRING, + loc, + }, + type: STRING, + loc, + }; +} + function lowerRegistration( lowerer: Lowerer, expr: ts.CallExpression, @@ -293,21 +436,11 @@ function lowerRegistration( sub: IrExpr | null, // the lowered TestContext receiver (t.test) ): IrExpr { if (sub === null) requireStatementPosition(lowerer, expr, what); - const args = expr.arguments; - if (args.length < 1 || args.length > 3) { - lowerer.noLowering( - `${what} with ${args.length} arguments`, - expr, - "the supported forms are (name), (name, fn), and (name, options, fn)", - ); - } - let name = lowerer.lowerExprExpecting(args[0]!, STRING); + const { nameNode, optionsNode, fnNode } = splitTestArgs(lowerer, expr, what); + let name = lowerTestName(lowerer, expr, what, nameNode, fnNode, loc); let opts = noOptions(); - let fnNode: ts.Expression | undefined; - if (args.length === 2) fnNode = args[1]; - if (args.length === 3) { - opts = lowerTestOptions(lowerer, args[1]!, what, false); - fnNode = args[2]; + if (optionsNode) { + opts = lowerTestOptions(lowerer, optionsNode, what, false); // The name runs before the option values. if (opts.pre.length > 0 && name.kind !== "strLit") { const slot = lowerer.declareHiddenLocal("%testName", STRING); @@ -315,11 +448,9 @@ function lowerRegistration( name = { kind: "varRef", localId: slot.id, type: STRING, loc }; } } - if (methodMode === 1 || methodMode === 2) opts.mode = methodMode; if (methodMode === 3) opts.only = true; const at = strLit(atStringOf(expr), loc); - const mode = numLit(opts.mode, loc); - const msg = opts.msg ?? strLit("", loc); + const [mode, msg] = directiveOf(opts, methodMode, loc); const flagBits = optionFlags(opts); const planTimeout = [ numLit(opts.plan, loc), @@ -387,27 +518,21 @@ function lowerSuite( methodMode: number, ): IrExpr { requireStatementPosition(lowerer, expr, what); - const args = expr.arguments; - if (args.length < 2 || args.length > 3) { - lowerer.noLowering( - `${what} with ${args.length} arguments`, - expr, - "the supported forms are (name, fn) and (name, options, fn)", - ); - } - let name = lowerer.lowerExprExpecting(args[0]!, STRING); + const split = splitTestArgs(lowerer, expr, what); + let name = lowerTestName(lowerer, expr, what, split.nameNode, split.fnNode, loc); let opts = noOptions(); - let fnNode = args[1]!; - if (args.length === 3) { - opts = lowerTestOptions(lowerer, args[1]!, what, true); - fnNode = args[2]!; + if (split.optionsNode) { + opts = lowerTestOptions(lowerer, split.optionsNode, what, true); if (opts.pre.length > 0 && name.kind !== "strLit") { const slot = lowerer.declareHiddenLocal("%testName", STRING); opts.pre.unshift({ kind: "varDecl", localId: slot.id, init: name, loc }); name = { kind: "varRef", localId: slot.id, type: STRING, loc }; } } - if (methodMode === 1 || methodMode === 2) opts.mode = methodMode; + const fnNode = split.fnNode; + if (!fnNode) { + lowerer.noLowering(`${what} without a body`, expr, "give the suite a body function"); + } const cb = lowerer.lowerExpr(fnNode); // The body may take the SuiteContext (flag 2, as test bodies take theirs). if ( @@ -429,8 +554,7 @@ function lowerSuite( ); } const at = strLit(atStringOf(expr), loc); - const mode = numLit(opts.mode, loc); - const msg = opts.msg ?? strLit("", loc); + const [mode, msg] = directiveOf(opts, methodMode, loc); // describe.only and { only: true } mark the suite (flag 4, as for tests). const flags = numLit( optionFlags(opts) | (methodMode === 3 ? 4 : 0) | (cb.type.params.length === 1 ? 2 : 0), diff --git a/tests/fixtures/node-test/cases/test-name-forms.ts b/tests/fixtures/node-test/cases/test-name-forms.ts new file mode 100644 index 000000000..915d68a79 --- /dev/null +++ b/tests/fixtures/node-test/cases/test-name-forms.ts @@ -0,0 +1,46 @@ +// 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 +// against the todo method twins. +import { describe, it, test } from "node:test"; + +function declaredTest(): void {} +const boundArrow = (): void => {}; +const emptyName = process.argv.length > 99 ? "never" : ""; + +test(function namedExpression() {}); +test(() => {}); +test(declaredTest); +test(boundArrow); +test("", boundArrow); +test(emptyName, declaredTest); +test({ skip: "options first" }, () => {}); +test({ todo: true }); +test(); +test("name and options", { skip: true }); + +describe(function namedSuite() { + it(() => {}); + it({ todo: "later" }, function insideTodo() {}); +}); +describe({ skip: "whole suite" }, () => { + it("not run", () => {}); +}); + +test("subtests", async (t) => { + await t.test(function namedSub() {}); + await t.test(() => {}); + await t.test({ skip: true }, () => {}); +}); + +test("skip then todo", { skip: "skip message", todo: "todo message" }, () => {}); +test("todo then skip", { todo: "todo message", skip: true }, () => {}); +test.todo("todo twin with skip", { skip: true }, () => {}); +it.skip("skip twin with todo", { todo: true }, () => {}); +// The twins override the option of the same name, and the message is the +// skip string, else the todo string. +test.skip("skip twin with a skip message", { skip: "option message" }, () => {}); +it.todo("todo twin with a todo message", { todo: "option message" }, () => {}); +it.skip("skip twin with a todo message", { todo: "todo message" }, () => {}); +test.todo("todo twin with a skip message", { skip: "skip message" }, () => {}); +test("skip false with a todo message", { skip: false, todo: "todo message" }, () => {}); From f75b7d8aabc07daa8095ffb96542959557d02569 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 03:22:05 -0500 Subject: [PATCH 2/2] 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. --- docs/content/limitations/builtin-apis/test-runner.mdx | 1 + packages/compiler/src/frontend/lowering/lower-test.ts | 6 ++++++ tests/fixtures/node-test/cases/test-name-forms.ts | 11 ++++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) 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);