From 5c004f7c80c3df6f269185ca7996917718da0318 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Wed, 13 May 2026 13:08:15 +0300 Subject: [PATCH] test_runner: fix hooks test context Signed-off-by: Moshe Atlow --- lib/internal/test_runner/harness.js | 13 +--- lib/internal/test_runner/test.js | 21 +++++- test/parallel/test-runner-get-test-context.js | 70 ++++++++++++++++++- tools/eslint-rules/must-call-assert.js | 4 +- 4 files changed, 91 insertions(+), 17 deletions(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index f1d46301b025db..16f9392bf7763b 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -21,7 +21,7 @@ const { }, } = require('internal/errors'); const { exitCodes: { kGenericUserError } } = internalBinding('errors'); -const { kCancelledByParent, Test, Suite, TestContext, SuiteContext } = require('internal/test_runner/test'); +const { kCancelledByParent, Test, Suite } = require('internal/test_runner/test'); const { parseCommandLine, reporterScope, @@ -439,16 +439,7 @@ function getTestContext() { if (test === undefined || test === reporterScope) { return undefined; } - // For hooks (hookType is set), return the test/suite being hooked (the parent) - const actualTest = test.hookType !== undefined ? test.parent : test; - if (actualTest === undefined) { - return undefined; - } - // Return SuiteContext for suites, TestContext for tests - if (actualTest instanceof Suite) { - return new SuiteContext(actualTest); - } - return new TestContext(actualTest); + return test.getCtx(); } module.exports = { diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 013ba85087f1a0..adea37c215a200 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -1233,8 +1233,14 @@ class Test extends AsyncResource { } } + #ctx; + getCtx() { + this.#ctx ??= new TestContext(this); + return this.#ctx; + } + getRunArgs() { - const ctx = new TestContext(this); + const ctx = this.getCtx(); return { __proto__: null, ctx, args: [ctx] }; } @@ -1699,6 +1705,11 @@ class TestHook extends Test { this.#args = args; return super.run(); } + + getCtx() { + return this.parentTest.getCtx(); + } + getRunArgs() { return this.#args; } @@ -1785,6 +1796,12 @@ class Suite extends Test { this.buildPhaseFinished = true; } + #ctx; + getCtx() { + this.#ctx ??= new TestContext(this); + return this.#ctx; + } + getRunArgs() { const ctx = new SuiteContext(this); return { __proto__: null, ctx, args: [ctx] }; @@ -1857,6 +1874,4 @@ module.exports = { kUnwrapErrors, Suite, Test, - TestContext, - SuiteContext, }; diff --git a/test/parallel/test-runner-get-test-context.js b/test/parallel/test-runner-get-test-context.js index bc5be4f01cee4a..1e5ef917f9fbac 100644 --- a/test/parallel/test-runner-get-test-context.js +++ b/test/parallel/test-runner-get-test-context.js @@ -1,7 +1,16 @@ 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('node:assert'); -const { test, getTestContext, describe, it } = require('node:test'); +const { + test, + getTestContext, + describe, + it, + before, + after, + beforeEach, + afterEach, +} = require('node:test'); // Outside a test — must return undefined assert.strictEqual(getTestContext(), undefined); @@ -40,6 +49,63 @@ describe('getTestContext returns SuiteContext in suite', () => { }); }); +describe('getTestContext inside hooks', () => { + const suiteName = 'getTestContext inside hooks'; + + before(common.mustCall((t) => { + const ctx = getTestContext(); + assert.ok(ctx !== undefined); + assert.strictEqual(ctx.name, suiteName); + assert.strictEqual(ctx.name, t.name); + })); + + beforeEach(common.mustCall(() => { + const ctx = getTestContext(); + assert.ok(ctx !== undefined); + assert.strictEqual(ctx.name, suiteName); + })); + + afterEach(common.mustCall(() => { + const ctx = getTestContext(); + assert.ok(ctx !== undefined); + assert.strictEqual(ctx.name, suiteName); + })); + + after(common.mustCall((t) => { + const ctx = getTestContext(); + assert.ok(ctx !== undefined); + assert.strictEqual(ctx.name, suiteName); + assert.strictEqual(ctx.name, t.name); + })); + + it('runs inside the suite', () => { + const ctx = getTestContext(); + assert.ok(ctx !== undefined); + assert.strictEqual(ctx.name, 'runs inside the suite'); + }); +}); + +test('getTestContext inside test-level hooks returns the parent test', async (t) => { + const parentName = t.name; + t.beforeEach(common.mustCall(() => { + const ctx = getTestContext(); + assert.ok(ctx !== undefined); + assert.strictEqual(ctx.name, parentName); + })); + + t.afterEach(common.mustCall(() => { + const ctx = getTestContext(); + assert.ok(ctx !== undefined); + assert.strictEqual(ctx.name, parentName); + })); + + await t.test('child', () => { + const ctx = getTestContext(); + assert.ok(ctx !== undefined); + assert.strictEqual(ctx.name, 'child'); + }); +}); + test('getTestContext works in test body during async operations', async (t) => { const ctx = getTestContext(); assert.ok(ctx !== undefined); diff --git a/tools/eslint-rules/must-call-assert.js b/tools/eslint-rules/must-call-assert.js index 7a6f417770bae0..b991e29063aa9c 100644 --- a/tools/eslint-rules/must-call-assert.js +++ b/tools/eslint-rules/must-call-assert.js @@ -63,7 +63,9 @@ function isMustCallOrMustCallAtLeast(str) { } function isMustCallOrTest(str) { - return str === 'test' || str === 'it' || isMustCallOrMustCallAtLeast(str); + return str === 'test' || str === 'it' || str === 'describe' || str === 'suite' || + str === 'before' || str === 'after' || str === 'beforeEach' || str === 'afterEach' || + isMustCallOrMustCallAtLeast(str); } module.exports = {