From 6d54eba8f9f5603a4c309d440e3bfdabaa3326e0 Mon Sep 17 00:00:00 2001 From: ThanhNguyxn Date: Sat, 7 Feb 2026 13:48:48 -0300 Subject: [PATCH] test_runner: exclude mocked CJS modules from coverage When a CommonJS module is mocked and imported into an ESM module, the coverage report incorrectly includes the mocked module. This happens because V8 coverage reports use the original file URL without the mock search parameter that ESM loader adds. This fix checks if the base URL (without query params) is registered in the mocks registry in addition to checking the URL search params. Fixes: https://github.com/nodejs/node/issues/61709 --- lib/internal/test_runner/coverage.js | 16 ++++++-- .../coverage-with-mock-cjs/dependency.cjs | 13 +++++++ .../coverage-with-mock-cjs/subject.mjs | 5 +++ .../output/coverage-with-mock-cjs.mjs | 17 +++++++++ .../output/coverage-with-mock-cjs.snapshot | 38 +++++++++++++++++++ .../test-output-coverage-with-mock-cjs.mjs | 25 ++++++++++++ 6 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/test-runner/coverage-with-mock-cjs/dependency.cjs create mode 100644 test/fixtures/test-runner/coverage-with-mock-cjs/subject.mjs create mode 100644 test/fixtures/test-runner/output/coverage-with-mock-cjs.mjs create mode 100644 test/fixtures/test-runner/output/coverage-with-mock-cjs.snapshot create mode 100644 test/test-runner/test-output-coverage-with-mock-cjs.mjs diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 8fa9c872568d1e..4f064d077c4775 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -37,7 +37,7 @@ const { }, } = require('internal/errors'); const { matchGlobPattern } = require('internal/fs/glob'); -const { constants: { kMockSearchParam } } = require('internal/test_runner/mock/loader'); +const { constants: { kMockSearchParam }, mocks } = require('internal/test_runner/mock/loader'); const kCoverageFileRegex = /^coverage-(\d+)-(\d{13})-(\d+)\.json$/; const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; @@ -498,8 +498,18 @@ class TestCoverage { return true; } - const searchParams = new URL(url).searchParams; - if (searchParams.get(kMockSearchParam)) { + const parsedURL = new URL(url); + if (parsedURL.searchParams.get(kMockSearchParam)) { + return true; + } + + // Check if the base URL (without query params) is registered as a mock. + // This handles the case where a CJS module is mocked and imported into + // an ESM module - V8 coverage reports the original file URL without the + // mock search parameter. + parsedURL.search = ''; + const mock = mocks.get(parsedURL.href); + if (mock?.active === true) { return true; } diff --git a/test/fixtures/test-runner/coverage-with-mock-cjs/dependency.cjs b/test/fixtures/test-runner/coverage-with-mock-cjs/dependency.cjs new file mode 100644 index 00000000000000..362067b06b2110 --- /dev/null +++ b/test/fixtures/test-runner/coverage-with-mock-cjs/dependency.cjs @@ -0,0 +1,13 @@ +'use strict'; + +const data = { type: 'cjs-object' }; + +function sum(a, b) { + return a + b; +} + +function getData() { + return data; +} + +module.exports = { sum, getData }; diff --git a/test/fixtures/test-runner/coverage-with-mock-cjs/subject.mjs b/test/fixtures/test-runner/coverage-with-mock-cjs/subject.mjs new file mode 100644 index 00000000000000..1a5ded9a3da1b7 --- /dev/null +++ b/test/fixtures/test-runner/coverage-with-mock-cjs/subject.mjs @@ -0,0 +1,5 @@ +import { sum, getData } from './dependency.cjs'; + +export const theModuleSum = (a, b) => sum(a, b); + +export const theModuleGetData = () => getData(); diff --git a/test/fixtures/test-runner/output/coverage-with-mock-cjs.mjs b/test/fixtures/test-runner/output/coverage-with-mock-cjs.mjs new file mode 100644 index 00000000000000..003304b7224a76 --- /dev/null +++ b/test/fixtures/test-runner/output/coverage-with-mock-cjs.mjs @@ -0,0 +1,17 @@ +import { describe, it, mock } from 'node:test'; + +describe('coverage with mocked CJS module in ESM', async () => { + mock.module('../coverage-with-mock-cjs/dependency.cjs', { + namedExports: { + sum: (a, b) => 42, + getData: () => ({ mocked: true }), + }, + }); + + const { theModuleSum, theModuleGetData } = await import('../coverage-with-mock-cjs/subject.mjs'); + + it('uses mocked CJS exports', (t) => { + t.assert.strictEqual(theModuleSum(1, 2), 42); + t.assert.deepStrictEqual(theModuleGetData(), { mocked: true }); + }); +}); diff --git a/test/fixtures/test-runner/output/coverage-with-mock-cjs.snapshot b/test/fixtures/test-runner/output/coverage-with-mock-cjs.snapshot new file mode 100644 index 00000000000000..83267c46cbf004 --- /dev/null +++ b/test/fixtures/test-runner/output/coverage-with-mock-cjs.snapshot @@ -0,0 +1,38 @@ +TAP version 13 +# Subtest: coverage with mocked CJS module in ESM + # Subtest: uses mocked CJS exports + ok 1 - uses mocked CJS exports + --- + duration_ms: * + type: 'test' + ... + 1..1 +ok 1 - coverage with mocked CJS module in ESM + --- + duration_ms: * + type: 'suite' + ... +1..1 +# tests 1 +# suites 1 +# pass 1 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms * +# start of coverage report +# ------------------------------------------------------------------------------- +# file | line % | branch % | funcs % | uncovered lines +# ------------------------------------------------------------------------------- +# test | | | | +# fixtures | | | | +# test-runner | | | | +# coverage-with-mock-cjs | | | | +# subject.mjs | 100.00 | 100.00 | 100.00 | +# output | | | | +# coverage-with-mock-cjs.mjs | 100.00 | 100.00 | 100.00 | +# ------------------------------------------------------------------------------- +# all files | 100.00 | 100.00 | 100.00 | +# ------------------------------------------------------------------------------- +# end of coverage report diff --git a/test/test-runner/test-output-coverage-with-mock-cjs.mjs b/test/test-runner/test-output-coverage-with-mock-cjs.mjs new file mode 100644 index 00000000000000..d751d20bc5341b --- /dev/null +++ b/test/test-runner/test-output-coverage-with-mock-cjs.mjs @@ -0,0 +1,25 @@ +// Test that mocked CJS modules imported into ESM are excluded from coverage. +// This tests the fix for https://github.com/nodejs/node/issues/61709 +import * as common from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import { spawnAndAssert, defaultTransform, ensureCwdIsProjectRoot } from '../common/assertSnapshot.js'; + +if (!process.features.inspector) { + common.skip('inspector support required'); +} + +ensureCwdIsProjectRoot(); +await spawnAndAssert( + fixtures.path('test-runner/output/coverage-with-mock-cjs.mjs'), + defaultTransform, + { + flags: [ + '--disable-warning=ExperimentalWarning', + '--test-reporter=tap', + '--experimental-transform-types', + '--experimental-test-module-mocks', + '--experimental-test-coverage', + '--test-coverage-exclude=!test/**', + ], + }, +);