Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions tests/e2e-coverage-report.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,136 @@ test('collectE2ECoverage maps V8 ranges through an external source map to src li
}
});

// v8-to-istanbul builds every region from a *generated* block boundary and maps
// its endpoints back through the source map. Minified output has no mapping at
// most boundaries, so an endpoint lands on whatever mapping precedes it and a
// block that never ran is reported as an original span that demonstrably did:
// at 900592b the report put a zero-count region across useFocusTrap.js lines
// 38-40 -- the Shift+Tab wrap arm that tests/e2e/interactions.spec.js:217
// presses and asserts -- while statement coverage put all three of those lines
// at 1 in the same union (#1035). Those spans inflated the denominator and sent
// contributors to write tests for branches that were already covered.
test('a zero-count region spanning only covered lines is not counted against the file', async () => {
const fixture = await fixtureRun();
try {
const lines = [
'const a = 1;',
'',
'const c = 3;',
'const d = 4;',
'const e = 5;',
];
const source = `${lines.join('\n')}\n`;
const scriptText = `${source}\n//# sourceMappingURL=app.js.map\n`;
const offset = (text) => source.indexOf(text);
const original = join(fixture.root, 'src/components/Example/index.js');
await mkdir(join(fixture.root, 'src/components/Example'), {
recursive: true,
});
await writeFile(original, source);
await writeFile(join(fixture.buildDir, 'assets/js/app.js'), scriptText);
await writeFile(
join(fixture.buildDir, 'assets/js/app.js.map'),
JSON.stringify({
version: 3,
file: 'app.js',
sources: ['webpack://endusers/./src/components/Example/index.js'],
sourcesContent: [source],
names: [],
// Every line maps at column 0; line 4 carries a second mapping at
// column 8, so a block opening mid-line resolves inside it rather
// than snapping back to the start of the line.
mappings: `${mapLines([
[0, 0],
[0, 1],
[0, 2],
[0, 3],
[0, 4],
])},${[vlq(8), vlq(0), vlq(0), vlq(8)].join('')}`,
}),
);

// One page runs everything but the last line.
await writeCoverageArtifact(fixture.runDir, 'worker-0-page-0', {
schemaVersion: 1,
kind: 'endusers.playwright.v8-coverage',
runId: 'run-1',
result: [
{
url: 'http://localhost:3000/assets/js/app.js',
scriptId: '1',
functions: [
{
functionName: '',
isBlockCoverage: true,
ranges: [
{
startOffset: 0,
endOffset: scriptText.length,
count: 1,
},
{
startOffset: offset('const e'),
endOffset: scriptText.length,
count: 0,
},
],
},
],
},
],
});
// A second page contributes two more zero-count blocks: one over lines
// 1-3, standing in for the mis-mapped multi-line span (line 2 is blank, so
// the span has to be judged on the lines that carry statements), and one
// from inside line 4 to the end of line 5, which closes on a line nothing
// ran. Only the first is a phantom.
await writeCoverageArtifact(fixture.runDir, 'worker-0-page-1', {
schemaVersion: 1,
kind: 'endusers.playwright.v8-coverage',
runId: 'run-1',
result: [
{
url: 'http://localhost:3000/assets/js/app.js',
scriptId: '2',
functions: [
{
functionName: '',
isBlockCoverage: true,
ranges: [
{ startOffset: 0, endOffset: scriptText.length, count: 1 },
{ startOffset: 0, endOffset: offset('const d'), count: 0 },
{
startOffset: offset('const d') + 'const d'.length,
endOffset: offset('const e') + 'const e = 5;'.length,
count: 0,
},
],
},
],
},
],
});
await sealCoverageRun(fixture.runDir, 'passed');

const report = await collectE2ECoverage(fixture.runDir, {
root: fixture.root,
buildDir: fixture.buildDir,
});
assert.equal(report.status, 'ok');
const [entry] = report.sources;
assert.equal(entry.file, 'src/components/Example/index.js');
assert.deepEqual(entry.uncoveredLines, [5]);
// Line 1 is gone: nothing in lines 1-3 went unexecuted, so no region can
// span them unexecuted. The span closing on line 5 and the region confined
// to line 5 both stay, and so does their weight in the denominator.
assert.deepEqual(entry.uncoveredRegions, [4, 5]);
assert.equal(entry.regions - entry.coveredRegions, 2);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});

// `npm run build:e2e:coverage` compiles the site twice: once from the real
// data, and once with tests/e2e/fixtures/data-variants/** layered on, into
// build/e2e-coverage-variant under its own base URL. That is what makes a
Expand Down
32 changes: 29 additions & 3 deletions tests/tools/e2e-coverage-report.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,29 @@ function getLineCoverage(coverageData) {
return lines;
}

// v8-to-istanbul derives every region from a *generated* block boundary and
// maps its endpoints back through the source map. In minified output those
// endpoints land on whatever mapping precedes them, so a block that never ran
// can be reported as a multi-line original span that demonstrably did run --
// `useFocusTrap`'s Shift+Tab arm reported a zero-count region over lines
// 38-40 while statement coverage put all three lines at 1 (#1035). A span
// cannot be unexecuted while every line it covers executed, so those spans are
// dropped rather than counted against the file. Single-line regions are kept:
// several of them share one line and the per-line fold cannot tell them apart,
// which is the whole reason regions are measured.
function isPhantomRegion(region, lines) {
if (region.count > 0) return false;
if (region.endLine <= region.line) return false;
for (let line = region.line; line <= region.endLine; line += 1) {
const count = lines.get(line);
// Blank lines and comments inside the span carry no statement, so they
// neither confirm nor contradict the region.
if (count === undefined) continue;
if (count <= 0) return false;
}
return true;
}

// A region is one branch location from the istanbul object convertScript
// already builds -- the arm of a ternary, a short-circuit operand, a default
// parameter -- data the line map cannot see because several of them share a
Expand All @@ -320,16 +343,17 @@ function getRegionCoverage(coverageData) {
for (const [index, location] of locations.entries()) {
const line = location?.start?.line;
if (!Number.isInteger(line)) continue;
const endLine = location.end?.line ?? line;
const key = [
line,
location.start?.column ?? 0,
location.end?.line ?? line,
endLine,
location.end?.column ?? 0,
].join(':');
const count = counts?.[index] ?? 0;
const existing = regions.get(key);
if (!existing || count > existing.count) {
regions.set(key, { line, count });
regions.set(key, { line, endLine, count });
}
}
}
Expand Down Expand Up @@ -551,7 +575,9 @@ export async function collectE2ECoverage(
const coveredLines = [...coverage.lines.values()].filter(
(count) => count > 0,
).length;
const regionValues = [...coverage.regions.values()];
const regionValues = [...coverage.regions.values()].filter(
(region) => !isPhantomRegion(region, coverage.lines),
);
const regions = regionValues.length;
const coveredRegions = regionValues.filter(
(region) => region.count > 0,
Expand Down
Loading