Pass the SuiteContext and hook contexts (#1302)

This commit is contained in:
Chris Tate
2026-10-18 10:54:53 -05:00
committed by GitHub
12 changed files with 232 additions and 99 deletions
@@ -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, `<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`.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "622f48ed5c836aec4644",
"artifactVersion": "a610bdd1ae56e1c4cd38",
"rowCount": 3880
}
+20 -20
View File
@@ -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"
}
},
{
@@ -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"
]
}
}
},
@@ -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"
},
@@ -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"] },
+13 -2
View File
@@ -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<void>;
type SuiteFn = () => void;
type HookFn = () => void | Promise<void>;
type SuiteFn = (s: SuiteContext) => void;
type HookFn = (c: TestContext | SuiteContext) => void | Promise<void>;
/* 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,
@@ -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",
@@ -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(
@@ -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],
+18 -9
View File
@@ -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();
+57
View File
@@ -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",
]);
});