From c73f36390627c7a94c3eaace80d6ca9764ad8b19 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 10:31:11 -0500 Subject: [PATCH] Pass the SuiteContext and hook contexts 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. --- .../limitations/builtin-apis/test-runner.mdx | 1 + .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 40 ++++---- .../generated/node-v24-backlog.json | 94 +++++++++++-------- .../generated/node-v24-internal.json | 53 ++++++++--- internal/compatibility/static-support.json | 1 + .../ambient/scriptc-node-fallback.d.ts | 15 ++- .../src/frontend/lowering/lower-test.ts | 25 +++-- packages/compiler/src/frontend/type-mapper.ts | 10 +- .../compiler/src/ir/builtin-signatures.ts | 6 +- packages/runtime/src/scr_test.c | 27 ++++-- .../fixtures/node-test/cases/suite-context.ts | 57 +++++++++++ 12 files changed, 232 insertions(+), 99 deletions(-) create mode 100644 tests/fixtures/node-test/cases/suite-context.ts diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 76d3a5c16..e1baac963 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -1,3 +1,4 @@ - **Concurrent tests:** with `{ concurrency }` above 1, subtests run on their own fibers, and results are reported in declaration order, as in Node. Concurrent bodies resume in Node's order around `beforeEach` hooks and nested suites. Other runner steps, such as `afterEach` and `after` hooks, take no microtask turns between bodies, so code that observes the interleaving of concurrent bodies across those steps can see a different order than Node. - **Snapshots:** `t.assert.snapshot` and `t.assert.fileSnapshot` compile, with Node's default serializer, `JSON.stringify(value, null, 2)`, or an `options.serializers` array literal. A compiled test reads the snapshot file Node writes beside the entry file, `.snapshot`, at the entry file's path when the program was compiled. Running the binary with Node's flag `--test-update-snapshots` as its first argument writes the snapshot files instead; as in Node, the flag is kept out of `process.argv` and appears in `process.execArgv`. Snapshot files are read in the form Node writes them, `` exports[`key`] = `value`; `` statements. The `snapshot.setDefaultSnapshotSerializers` and `snapshot.setResolveSnapshotPath` module functions throw `SC2020`. - **TestContext values:** `t.filePath` is the entry file's path when the program was compiled. `t.attempt` is `0`, since a test runs once. `t.passed` is `false` while the body runs and the test's outcome in its `afterEach` and `t.after` hooks. `t.workerId` is `Number(process.env.NODE_TEST_WORKER_ID) || undefined`, as in Node. `t.tags` is `[]`, since the `tags` test option throws `SC2020`; the array is not frozen as Node's is. +- **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`. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 35cbcc65e..a64169933 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "622f48ed5c836aec4644", + "artifactVersion": "a610bdd1ae56e1c4cd38", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index d84bdfdfa..d4b6839e7 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -689,9 +689,9 @@ "apiEntries": 117, "static": { "supported": 0, - "partial": 58, + "partial": 64, "refused": 0, - "not-implemented": 59, + "not-implemented": 53, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -66677,9 +66677,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66723,9 +66723,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66746,9 +66746,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66769,9 +66769,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66815,9 +66815,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66838,9 +66838,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 3a8eca639..f36c865cf 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -7,8 +7,8 @@ "tierItemCount": 3456, "tiers": { "static": { - "audit-partial": 1149, - "verify-gap": 2180, + "audit-partial": 1155, + "verify-gap": 2174, "replace-refusal": 40, "classify": 2, "implement": 85 @@ -68013,12 +68013,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.diagnostic", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68063,12 +68066,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.filePath", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68088,12 +68094,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.fullName", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68113,12 +68122,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.name", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68163,12 +68175,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.passed", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68188,12 +68203,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.attempt", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 327e41a49..514282a0a 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -746,9 +746,9 @@ "apiEntries": 117, "static": { "supported": 0, - "partial": 58, + "partial": 64, "refused": 0, - "not-implemented": 59, + "not-implemented": 53, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -90980,6 +90980,7 @@ "tests/fixtures/node-test/cases/mock-timers.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/snapshot.ts", + "tests/fixtures/node-test/cases/suite-context.ts", "tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts" @@ -93943,8 +93944,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.diagnostic", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93989,8 +93994,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.filePath", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -94012,8 +94021,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.fullName", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -94035,8 +94048,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.name", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -94081,8 +94098,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.passed", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "ancestor" }, @@ -94104,8 +94125,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.attempt", + "tests": [ + "tests/fixtures/node-test/cases/suite-context.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "ancestor" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index c87bb257a..45a946925 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -357,6 +357,7 @@ { "chapter": "url", "signatures": ["Legacy `urlObject`", "`urlObject.auth`", "`urlObject.hash`", "`urlObject.host`", "`urlObject.hostname`", "`urlObject.href`", "`urlObject.path`", "`urlObject.pathname`", "`urlObject.port`", "`urlObject.protocol`", "`urlObject.query`", "`urlObject.search`", "`urlObject.slashes`"], "status": "partial", "evidence": ["tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5224-url-format-objects.ts", "tests/fixtures/node-types/url-legacy.ts"] }, { "chapter": "test", "parent": "`context.assert`", "symbols": ["context.assert.snapshot", "context.assert.fileSnapshot"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/snapshot.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.filePath", "passed", "attempt", "tags", "workerId"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-members.ts", "tests/harness/node-test.test.ts"] }, + { "chapter": "test", "parent": "Class: `SuiteContext`", "symbols": ["context.name", "context.fullName", "context.filePath", "context.diagnostic", "passed", "attempt"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-context.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.diagnostic", "context.name"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.method", "mock.reset", "mock.restoreAll"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-method.ts", "tests/corpus/5810-mock-method-records.ts", "tests/corpus/5811-mock-tracker-restore.ts", "tests/harness/node-test.test.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 06938ad18..d72ca2dcd 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3375,8 +3375,18 @@ declare module "node:util/types" { * suites typecheck but fence at their use sites (no lowering). */ declare module "node:test" { type TestFn = (t: TestContext) => void | Promise; - type SuiteFn = () => void; - type HookFn = () => void | Promise; + type SuiteFn = (s: SuiteContext) => void; + type HookFn = (c: TestContext | SuiteContext) => void | Promise; + /* The describe/suite callback's context, and the before/after hook + * context of a suite. */ + interface SuiteContext { + readonly name: string; + readonly fullName: string; + readonly filePath: string | undefined; + readonly attempt: number; + readonly passed: boolean; + diagnostic(message: string): void; + } interface TestOptions { skip?: boolean | string; todo?: boolean | string; @@ -3537,6 +3547,7 @@ declare module "node:test" { run, snapshot, TestContext, + SuiteContext, TestOptions, Mock, MockFunctionContext, diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index 84be23b67..b9d4e8df1 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -409,11 +409,16 @@ function lowerSuite( } if (methodMode === 1 || methodMode === 2) opts.mode = methodMode; const cb = lowerer.lowerExpr(fnNode); - if (cb.type.kind !== "func" || cb.type.params.length !== 0) { + // The body may take the SuiteContext (flag 2, as test bodies take theirs). + if ( + cb.type.kind !== "func" || + cb.type.params.length > 1 || + (cb.type.params.length === 1 && cb.type.params[0]!.kind !== "testCtx") + ) { lowerer.noLowering( - `${what} whose body is not a zero-parameter function`, + `${what} whose body is not a function of the SuiteContext`, fnNode, - "suite bodies take no parameters (they run at registration)", + "suite bodies take no parameters or the SuiteContext (they run at registration)", ); } if (cb.type.ret.kind !== "void" && !isUnitType(cb.type.ret)) { @@ -427,7 +432,10 @@ function lowerSuite( const mode = numLit(opts.mode, loc); const msg = opts.msg ?? strLit("", loc); // describe.only and { only: true } mark the suite (flag 4, as for tests). - const flags = numLit(optionFlags(opts) | (methodMode === 3 ? 4 : 0), loc); + const flags = numLit( + optionFlags(opts) | (methodMode === 3 ? 4 : 0) | (cb.type.params.length === 1 ? 2 : 0), + loc, + ); return withOptionValues( { kind: "libCall", @@ -456,10 +464,12 @@ function lowerHook( lowerer.noLowering( `${what} with ${args.length} arguments`, expr, - "the supported form is one zero-parameter function (hook options have no lowering)", + "the supported form is one function (hook options have no lowering)", ); } - const { cb, flags } = lowerBodyArg(lowerer, args[0]!, what, false); + // A hook may take its context: the suite's for before/after, the test's + // for beforeEach/afterEach. + const { cb, flags } = lowerBodyArg(lowerer, args[0]!, what, true); return { kind: "libCall", fn: "test.hook", @@ -919,7 +929,8 @@ export function lowerTestMethodCall( ); } const receiver = lowerer.lowerExpr(access.expression); - const { cb, flags } = lowerBodyArg(lowerer, args[0]!, what, false); + // The hook may take the test's context. + const { cb, flags } = lowerBodyArg(lowerer, args[0]!, what, true); return { kind: "libCall", fn: "test.ctxHook", diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 36cf9a7fb..5ecfc60ae 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2952,11 +2952,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return { kind: "dgramSocket" }; } // node:test's TestContext — the test-body parameter (`test('x', (t) => - // ...)`). @types/node's `class TestContext` and the fallback - // declarations' interface both live inside `declare module "node:test"` - // (isDeclaredInAmbientModule answers for both spellings). + // ...)`) — and SuiteContext, the describe-body and suite-hook parameter: + // both are the runner's node for the test or suite. @types/node's + // classes and the fallback declarations' interfaces all live inside + // `declare module "node:test"` (isDeclaredInAmbientModule answers for + // both spellings). if ( - psym?.name === "TestContext" && + (psym?.name === "TestContext" || psym?.name === "SuiteContext") && checker .declarationsOf(psym) .some( diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index db5e2c619..780918333 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1436,10 +1436,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [STRING, F64, STRING, F64, STRING, F64, F64, F64], result: VOID, }, - "test.suite": { - argTypes: [STRING, F64, STRING, { kind: "func", params: [], ret: VOID }, STRING, F64, F64], - result: VOID, - }, + // The suite body takes no parameters or its SuiteContext (flag 2). + "test.suite": { argTypes: [STRING, F64, STRING, null, STRING, F64, F64], result: VOID }, "test.hook": { argTypes: [F64, null, F64], result: VOID }, "test.sub": { argTypes: [TESTCTX_T, STRING, F64, STRING, null, F64, STRING, F64, F64, F64], diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index f40827e42..cb2c81971 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -406,9 +406,12 @@ static void scr_test_mocks_restore(ScrTestHooks *mocks, bool forget) { /* Run `which` hooks up (beforeEach: outermost suite first) or down * (afterEach: innermost first) the ancestor chain. A hook throw fails the * test with the hook's error (Node cancels harder — SEMANTICS.md). */ -static void scr_test_run_hooks_of(ScrTestCtx *owner, int which) { +/* `ctx` is the context a hook that takes one receives: the suite or test + * for its own before/after hooks, the test for the beforeEach/afterEach + * chain. */ +static void scr_test_run_hooks_of(ScrTestCtx *owner, int which, ScrTestCtx *ctx) { for (size_t i = 0; i < owner->hooks[which].n && !scr_exc_pending(); i++) { - scr_test_call(owner->hooks[which].v[i].cb, owner->hooks[which].v[i].flags, NULL, NULL); + scr_test_call(owner->hooks[which].v[i].cb, owner->hooks[which].v[i].flags, ctx, NULL); } } @@ -422,9 +425,9 @@ static bool scr_test_run_each_hooks(ScrTestCtx *t, int which, bool outermost_fir if (p->hooks[which].n > 0) any = true; } if (outermost_first) { - for (int i = n - 1; i >= 0; i--) scr_test_run_hooks_of(chain[i], which); + for (int i = n - 1; i >= 0; i--) scr_test_run_hooks_of(chain[i], which, t); } else { - for (int i = 0; i < n; i++) scr_test_run_hooks_of(chain[i], which); + for (int i = 0; i < n; i++) scr_test_run_hooks_of(chain[i], which, t); } return any; } @@ -595,7 +598,7 @@ static void scr_test_run_node(ScrTestCtx *t) { /* The body already ran at registration (children collected). A hook * throw fails the SUITE with that error — before-failures skip the * children (Node cancels them; SEMANTICS.md numbers the difference). */ - scr_test_run_hooks_of(t, SCR_HOOK_BEFORE); + scr_test_run_hooks_of(t, SCR_HOOK_BEFORE, t); if (scr_exc_pending()) { t->err = scr_exc_take(); } else { @@ -606,7 +609,7 @@ static void scr_test_run_node(ScrTestCtx *t) { } else { scr_test_run_children(t); } - scr_test_run_hooks_of(t, SCR_HOOK_AFTER); + scr_test_run_hooks_of(t, SCR_HOOK_AFTER, t); if (scr_exc_pending()) t->err = scr_exc_take(); } t->dur_ms = scr_now_ms() - start; @@ -619,6 +622,8 @@ static void scr_test_run_node(ScrTestCtx *t) { : scr_test_subtree_failed(t) ? SCR_TS_CHILDFAIL : SCR_TS_PASS; } + /* Node's suite pass() comes after its after hooks. */ + t->passed = t->state == SCR_TS_PASS || t->state == SCR_TS_TODO_PASS; scr_test_report_result(t); return; } @@ -678,7 +683,7 @@ static void scr_test_run_node(ScrTestCtx *t) { } } /* t.after hooks run once the test and its afterEach chain finish. */ - scr_test_run_hooks_of(t, SCR_HOOK_AFTER); + scr_test_run_hooks_of(t, SCR_HOOK_AFTER, t); if (scr_exc_pending()) { if (!failed && !t->err) { t->err = scr_exc_take(); @@ -1519,7 +1524,11 @@ void scr_test_suite(ScrStr *name /*borrowed*/, double mode, ScrStr *msg /*borrow if (cb && (int)mode != SCR_TEST_SKIP) { ScrTestCtx *prev = scr_test_parent_get(); scr_test_parent_set(t); - ((void (*)(ScrClosure *))cb->fn)(cb); + /* flag 2: the body takes its SuiteContext (+1, as test bodies do). */ + if ((int)flags & SCR_TEST_F_CTX) + ((void (*)(ScrClosure *, ScrTestCtx *))cb->fn)(cb, scr_testctx_retain(t)); + else + ((void (*)(ScrClosure *))cb->fn)(cb); scr_test_parent_set(prev); } scr_closure_release(cb); @@ -1597,7 +1606,7 @@ void scr_test_ctx_todo(ScrTestCtx *t, ScrStr *msg /*borrowed*/) { * each descendant subtest, and after runs once the test finishes. */ void scr_test_ctx_hook(ScrTestCtx *t, double which, ScrClosure *cb /*moves*/, double flags) { if ((int)which == SCR_HOOK_BEFORE) { - scr_test_call(cb, (int)flags, NULL, NULL); + scr_test_call(cb, (int)flags, t, NULL); scr_closure_release(cb); if (scr_exc_pending() && !t->before_err) t->before_err = scr_exc_take(); else if (scr_exc_pending()) scr_exc_clear(); diff --git a/tests/fixtures/node-test/cases/suite-context.ts b/tests/fixtures/node-test/cases/suite-context.ts new file mode 100644 index 000000000..5a13136ff --- /dev/null +++ b/tests/fixtures/node-test/cases/suite-context.ts @@ -0,0 +1,57 @@ +// The SuiteContext a describe body receives (name, fullName, filePath, +// attempt, passed, diagnostic), and the context hooks receive: before and +// after get the suite's, beforeEach and afterEach the test's, and a test's +// t.before and t.after hooks the test's. +import { after, afterEach, before, beforeEach, describe, it, test } from "node:test"; +import * as assert from "node:assert"; + +const seen: string[] = []; +let outerPassed = (): boolean => false; + +describe("outer", (s) => { + const file = (s.filePath ?? "").endsWith("tests/fixtures/node-test/cases/suite-context.ts"); + seen.push(`body: ${s.name} | ${s.fullName} | ${s.passed} | ${s.attempt} | ${file}`); + s.diagnostic("suite diagnostic"); + outerPassed = () => s.passed; + before((c) => { + seen.push(`before: ${c.name}`); + }); + after((c) => { + seen.push(`after: ${c.name} ${c.passed}`); + }); + beforeEach((c) => { + seen.push(`beforeEach: ${c.name}`); + }); + afterEach((c) => { + seen.push(`afterEach: ${c.name} ${c.passed}`); + }); + describe("inner", (s2) => { + seen.push(`inner: ${s2.fullName}`); + it("x", () => {}); + }); + it("y", (t) => { + t.before((c) => { + seen.push(`t.before: ${c.name}`); + }); + t.after((c) => { + seen.push(`t.after: ${c.name} ${c.passed}`); + }); + }); +}); + +test("report", () => { + seen.push(`outer afterwards: ${outerPassed()}`); + assert.deepStrictEqual(seen, [ + "body: outer | outer | false | 0 | true", + "inner: outer > inner", + "before: outer", + "beforeEach: x", + "afterEach: x true", + "beforeEach: y", + "t.before: y", + "afterEach: y true", + "t.after: y true", + "after: outer false", + "outer afterwards: true", + ]); +});