diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index baa80aa7df3715..1c37450a569d36 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -36,14 +36,14 @@ const kLineSplitRegex = /(?<=\r?\n)/u; const kStatusRegex = /\/\* node:coverage (?enable|disable) \*\//; class CoverageLine { - constructor(line, src, startOffset) { - const newlineLength = + constructor(line, startOffset, src, length = src?.length) { + const newlineLength = src == null ? 0 : RegExpPrototypeExec(kLineEndingRegex, src)?.[0].length ?? 0; this.line = line; this.src = src; this.startOffset = startOffset; - this.endOffset = startOffset + src.length - newlineLength; + this.endOffset = startOffset + length - newlineLength; this.ignore = false; this.count = this.startOffset === this.endOffset ? 1 : 0; } @@ -83,7 +83,7 @@ class TestCoverage { const lines = ArrayPrototypeMap(linesWithBreaks, (line, i) => { const startOffset = offset; - const coverageLine = new CoverageLine(i + 1, line, startOffset); + const coverageLine = new CoverageLine(i + 1, startOffset, line); offset += line.length; @@ -335,8 +335,13 @@ class TestCoverage { newResult.set(url, script); continue; } - const originalLines = this.getLines(url); const { data, lineLengths } = sourceMapCache[url]; + let offset = 0; + const executedLines = ArrayPrototypeMap(lineLengths, (length, i) => { + const coverageLine = new CoverageLine(i + 1, offset, null, length); + offset += length; + return coverageLine; + }); if (data.sourcesContent != null) { for (let j = 0; j < data.sources.length; ++j) { this.getLines(data.sources[j], data.sourcesContent[j]); @@ -353,7 +358,7 @@ class TestCoverage { const newRanges = []; for (let k = 0; k < ranges.length; ++k) { const { startOffset, endOffset, count } = ranges[k]; - const { lines } = mapRangeToLines(ranges[k], originalLines); + const { lines } = mapRangeToLines(ranges[k], executedLines); let startEntry = sourceMap .findEntry(lines[0].line - 1, MathMax(0, startOffset - lines[0].startOffset)); diff --git a/test/fixtures/test-runner/coverage-loader/hooks.mjs b/test/fixtures/test-runner/coverage-loader/hooks.mjs index 1aa04d0b45589d..c2e4b1dfc94628 100644 --- a/test/fixtures/test-runner/coverage-loader/hooks.mjs +++ b/test/fixtures/test-runner/coverage-loader/hooks.mjs @@ -1,11 +1,25 @@ -const source = ` +const sources = { +// Virtual file. Dosen't exist on disk + "virtual.js": ` import { test } from 'node:test'; test('test', async () => {}); -`; +`, +// file with source map. this emulates the behavior of tsx + "sum.test.ts": `\ +import{describe,it}from"node:test";import assert from"node:assert";import{sum}from"./sum.ts";describe("sum",()=>{it("should sum two numbers",()=>{assert.deepStrictEqual(sum(1,2),3)});it("should error out if one is not a number",()=>{assert.throws(()=>sum(1,"b"),Error)})}); + +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQUEsT0FBUyxTQUFVLE9BQVUsWUFDN0IsT0FBTyxXQUFZLGNBQ25CLE9BQVMsUUFBVyxRQUVwQixTQUFTLE1BQU8sSUFBTSxDQUNwQixHQUFHLHlCQUEwQixJQUFNLENBQy9CLE9BQU8sZ0JBQWdCLElBQUksRUFBRSxDQUFDLEVBQUcsQ0FBQyxDQUN0QyxDQUFDLEVBRUQsR0FBRywwQ0FBMkMsSUFBTSxDQUNsRCxPQUFPLE9BQU8sSUFBTSxJQUFJLEVBQUcsR0FBRyxFQUFHLEtBQUssQ0FDeEMsQ0FBQyxDQUNILENBQUMiLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOltdLCJzb3VyY2VzIjpbIi4vc3VtLnRlc3QudHMiXSwic291cmNlc0NvbnRlbnQiOltudWxsXX0=`, +// file with source map. this emulates the behavior of tsx + "sum.ts": `\ + var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:true});function sum(...n){if(!n.every(num=>typeof num==="number"))throw new Error("Not a number");return n.reduce((acc,cur)=>acc+cur)}__name(sum,"sum");export{sum}; + + //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6ImtIQUFPLFNBQVMsT0FBUSxFQUFHLENBQ3pCLEdBQUksQ0FBQyxFQUFFLE1BQU8sS0FBUSxPQUFPLE1BQVEsUUFBUSxFQUFHLE1BQU0sSUFBSSxNQUFNLGNBQWMsRUFDOUUsT0FBTyxFQUFFLE9BQU8sQ0FBQyxJQUFLLE1BQVEsSUFBTSxHQUFHLENBQ3pDLENBSGdCIiwibmFtZXMiOltdLCJpZ25vcmVMaXN0IjpbXSwic291cmNlcyI6WyIuL3N1bS50cyJdLCJzb3VyY2VzQ29udGVudCI6W251bGxdfQ==`, +}; export async function load(url, context, nextLoad) { - if (url.endsWith('virtual.js')) { - return { format: "module", source, shortCircuit: true }; + const file = url.split('/').at(-1); + if (sources[file] !== undefined) { + return { format: "module", source: sources[file], shortCircuit: true }; } return nextLoad(url, context); } diff --git a/test/fixtures/test-runner/coverage-loader/sum.test.ts b/test/fixtures/test-runner/coverage-loader/sum.test.ts new file mode 100644 index 00000000000000..1ee401c58016c8 --- /dev/null +++ b/test/fixtures/test-runner/coverage-loader/sum.test.ts @@ -0,0 +1,13 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert' +import { sum } from './sum' + +describe('sum', () => { + it('should sum two numbers', () => { + assert.deepStrictEqual(sum(1, 2), 3) + }) + + it('should error out if one is not a number', () => { + assert.throws(() => sum(1, 'b'), Error) + }) +}) diff --git a/test/fixtures/test-runner/coverage-loader/sum.ts b/test/fixtures/test-runner/coverage-loader/sum.ts new file mode 100644 index 00000000000000..3ea8cb645db87e --- /dev/null +++ b/test/fixtures/test-runner/coverage-loader/sum.ts @@ -0,0 +1,4 @@ +export function sum (...n) { + if (!n.every((num) => typeof num === 'number')) throw new Error('Not a number') + return n.reduce((acc, cur) => acc + cur) +} diff --git a/test/parallel/test-runner-coverage.js b/test/parallel/test-runner-coverage.js index 05057bf40838e8..45666a6389a08e 100644 --- a/test/parallel/test-runner-coverage.js +++ b/test/parallel/test-runner-coverage.js @@ -303,3 +303,35 @@ test('coverage with ESM hook - source irrelevant', skipIfNoInspector, () => { assert(result.stdout.toString().includes(report)); assert.strictEqual(result.status, 0); }); + +test('coverage with ESM hook - source transpiled', skipIfNoInspector, () => { + let report = [ + '# start of coverage report', + '# ------------------------------------------------------------------', + '# file | line % | branch % | funcs % | uncovered lines', + '# ------------------------------------------------------------------', + '# hooks.mjs | 100.00 | 100.00 | 100.00 | ', + '# register-hooks.js | 100.00 | 100.00 | 100.00 | ', + '# sum.test.ts | 100.00 | 100.00 | 100.00 | ', + '# sum.ts | 100.00 | 100.00 | 100.00 | ', + '# ------------------------------------------------------------------', + '# all files | 100.00 | 100.00 | 100.00 |', + '# ------------------------------------------------------------------', + '# end of coverage report', + ].join('\n'); + + if (common.isWindows) { + report = report.replaceAll('/', '\\'); + } + + const fixture = fixtures.path('test-runner', 'coverage-loader'); + const args = [ + '--import', './register-hooks.js', '--test', '--experimental-test-coverage', + '--test-reporter', 'tap', 'sum.test.ts', + ]; + const result = spawnSync(process.execPath, args, { cwd: fixture }); + + assert.strictEqual(result.stderr.toString(), ''); + assert(result.stdout.toString().includes(report)); + assert.strictEqual(result.status, 0); +});