@testdino/playwright 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +52 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +52 -3
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +262 -3
- package/dist/index.d.ts +262 -3
- package/dist/index.js +600 -50
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +593 -52
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -1
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { randomUUID } from 'crypto';
|
|
1
|
+
import { randomUUID, createHash } from 'crypto';
|
|
2
2
|
import { existsSync, readFileSync, statSync, createReadStream } from 'fs';
|
|
3
3
|
import WebSocket from 'ws';
|
|
4
4
|
import axios from 'axios';
|
|
5
|
-
import {
|
|
5
|
+
import { basename, extname, relative } from 'path';
|
|
6
|
+
import { rm, mkdir, readFile } from 'fs/promises';
|
|
6
7
|
import { execa } from 'execa';
|
|
7
8
|
import { type, release, platform, cpus, totalmem, hostname } from 'os';
|
|
8
9
|
import { version } from 'process';
|
|
9
|
-
import
|
|
10
|
+
import istanbulCoverage from 'istanbul-lib-coverage';
|
|
11
|
+
import { createContext } from 'istanbul-lib-report';
|
|
12
|
+
import { create } from 'istanbul-reports';
|
|
13
|
+
import { test as test$1 } from '@playwright/test';
|
|
14
|
+
export { expect } from '@playwright/test';
|
|
15
|
+
import chalk from 'chalk';
|
|
10
16
|
|
|
11
17
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
12
18
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -366,8 +372,12 @@ var WebSocketClient = class {
|
|
|
366
372
|
}
|
|
367
373
|
}
|
|
368
374
|
};
|
|
369
|
-
|
|
370
|
-
|
|
375
|
+
function normalizePath(filePath, rootDir) {
|
|
376
|
+
if (rootDir && filePath.startsWith(rootDir)) {
|
|
377
|
+
return relative(rootDir, filePath);
|
|
378
|
+
}
|
|
379
|
+
return filePath;
|
|
380
|
+
}
|
|
371
381
|
function sleep(ms) {
|
|
372
382
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
373
383
|
}
|
|
@@ -1514,7 +1524,7 @@ var PlaywrightMetadataCollector = class extends BaseMetadataCollector {
|
|
|
1514
1524
|
const skeletonSuite = {
|
|
1515
1525
|
title: childSuite.title,
|
|
1516
1526
|
type: childSuite.type === "file" ? "file" : "describe",
|
|
1517
|
-
tests: childSuite.tests.map((
|
|
1527
|
+
tests: childSuite.tests.map((test2) => this.buildSkeletonTest(test2))
|
|
1518
1528
|
};
|
|
1519
1529
|
if (childSuite.type === "file" && childSuite.location) {
|
|
1520
1530
|
skeletonSuite.file = childSuite.location.file;
|
|
@@ -1536,24 +1546,24 @@ var PlaywrightMetadataCollector = class extends BaseMetadataCollector {
|
|
|
1536
1546
|
/**
|
|
1537
1547
|
* Build skeleton test from TestCase
|
|
1538
1548
|
*/
|
|
1539
|
-
buildSkeletonTest(
|
|
1549
|
+
buildSkeletonTest(test2) {
|
|
1540
1550
|
const skeletonTest = {
|
|
1541
|
-
testId:
|
|
1542
|
-
title:
|
|
1551
|
+
testId: test2.id,
|
|
1552
|
+
title: test2.title,
|
|
1543
1553
|
location: {
|
|
1544
|
-
file:
|
|
1545
|
-
line:
|
|
1546
|
-
column:
|
|
1554
|
+
file: test2.location.file,
|
|
1555
|
+
line: test2.location.line,
|
|
1556
|
+
column: test2.location.column
|
|
1547
1557
|
}
|
|
1548
1558
|
};
|
|
1549
|
-
if (
|
|
1550
|
-
skeletonTest.tags =
|
|
1559
|
+
if (test2.tags && test2.tags.length > 0) {
|
|
1560
|
+
skeletonTest.tags = test2.tags;
|
|
1551
1561
|
}
|
|
1552
|
-
if (
|
|
1553
|
-
skeletonTest.expectedStatus =
|
|
1562
|
+
if (test2.expectedStatus) {
|
|
1563
|
+
skeletonTest.expectedStatus = test2.expectedStatus;
|
|
1554
1564
|
}
|
|
1555
|
-
if (
|
|
1556
|
-
skeletonTest.annotations =
|
|
1565
|
+
if (test2.annotations && test2.annotations.length > 0) {
|
|
1566
|
+
skeletonTest.annotations = test2.annotations.map((ann) => ({
|
|
1557
1567
|
type: ann.type,
|
|
1558
1568
|
description: ann.description
|
|
1559
1569
|
}));
|
|
@@ -1924,8 +1934,324 @@ var ArtifactUploader = class {
|
|
|
1924
1934
|
return this.sasToken.uniqueId;
|
|
1925
1935
|
}
|
|
1926
1936
|
};
|
|
1927
|
-
|
|
1928
|
-
|
|
1937
|
+
function toIstanbulMapData(map) {
|
|
1938
|
+
return map;
|
|
1939
|
+
}
|
|
1940
|
+
function fromIstanbulMapData(data) {
|
|
1941
|
+
return data;
|
|
1942
|
+
}
|
|
1943
|
+
var CoverageMerger = class {
|
|
1944
|
+
coverageMap = istanbulCoverage.createCoverageMap({});
|
|
1945
|
+
hasData = false;
|
|
1946
|
+
includePatterns;
|
|
1947
|
+
excludePatterns;
|
|
1948
|
+
onError;
|
|
1949
|
+
constructor(options) {
|
|
1950
|
+
this.includePatterns = options?.include;
|
|
1951
|
+
this.excludePatterns = options?.exclude;
|
|
1952
|
+
this.onError = options?.onError;
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Merge a coverage fragment from a completed test.
|
|
1956
|
+
* The fragment is merged directly and can be GC'd immediately.
|
|
1957
|
+
*/
|
|
1958
|
+
addFragment(fragment) {
|
|
1959
|
+
if (!fragment.istanbul) return;
|
|
1960
|
+
try {
|
|
1961
|
+
const filtered = this.filterCoverageMap(fragment.istanbul);
|
|
1962
|
+
this.coverageMap.merge(toIstanbulMapData(filtered));
|
|
1963
|
+
if (this.coverageMap.files().length > 0) {
|
|
1964
|
+
this.hasData = true;
|
|
1965
|
+
}
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
const msg = `[TestDino] Failed to merge coverage fragment: ${error instanceof Error ? error.message : String(error)}`;
|
|
1968
|
+
if (this.onError) {
|
|
1969
|
+
this.onError(msg);
|
|
1970
|
+
} else {
|
|
1971
|
+
console.warn(msg);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Filter files from a coverage map based on include/exclude patterns.
|
|
1977
|
+
*/
|
|
1978
|
+
filterCoverageMap(coverageMap) {
|
|
1979
|
+
const hasInclude = this.includePatterns && this.includePatterns.length > 0;
|
|
1980
|
+
const hasExclude = this.excludePatterns && this.excludePatterns.length > 0;
|
|
1981
|
+
if (!hasInclude && !hasExclude) return coverageMap;
|
|
1982
|
+
const filtered = {};
|
|
1983
|
+
for (const [filePath, fileCoverage] of Object.entries(coverageMap)) {
|
|
1984
|
+
if (hasExclude && this.excludePatterns.some((pattern) => filePath.includes(pattern))) {
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
if (hasInclude && !this.includePatterns.some((pattern) => filePath.includes(pattern))) {
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
filtered[filePath] = fileCoverage;
|
|
1991
|
+
}
|
|
1992
|
+
return filtered;
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Whether any coverage data has been collected.
|
|
1996
|
+
*/
|
|
1997
|
+
get hasCoverage() {
|
|
1998
|
+
return this.hasData;
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Compute aggregate summary metrics from the merged coverage map.
|
|
2002
|
+
*/
|
|
2003
|
+
computeSummary() {
|
|
2004
|
+
const globalSummary = this.coverageMap.getCoverageSummary();
|
|
2005
|
+
return {
|
|
2006
|
+
lines: extractMetric(globalSummary.lines),
|
|
2007
|
+
branches: extractMetric(globalSummary.branches),
|
|
2008
|
+
functions: extractMetric(globalSummary.functions),
|
|
2009
|
+
statements: extractMetric(globalSummary.statements)
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Compute per-file coverage metrics.
|
|
2014
|
+
* Normalizes file paths to be relative to git root.
|
|
2015
|
+
*/
|
|
2016
|
+
computeFileCoverage(gitRoot) {
|
|
2017
|
+
const root = gitRoot || process.cwd();
|
|
2018
|
+
return this.coverageMap.files().map((filePath) => {
|
|
2019
|
+
const fileCoverage = this.coverageMap.fileCoverageFor(filePath);
|
|
2020
|
+
const fileSummary = fileCoverage.toSummary();
|
|
2021
|
+
const normalizedPath = normalizePath(filePath, root);
|
|
2022
|
+
return {
|
|
2023
|
+
path: normalizedPath,
|
|
2024
|
+
lines: extractMetric(fileSummary.lines),
|
|
2025
|
+
branches: extractMetric(fileSummary.branches),
|
|
2026
|
+
functions: extractMetric(fileSummary.functions),
|
|
2027
|
+
statements: extractMetric(fileSummary.statements)
|
|
2028
|
+
};
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Get the raw merged coverage map (for local report generation or compact extraction).
|
|
2033
|
+
*/
|
|
2034
|
+
getRawCoverageMap() {
|
|
2035
|
+
return this.coverageMap;
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Get the merged coverage map as a plain JSON object.
|
|
2039
|
+
*/
|
|
2040
|
+
toJSON() {
|
|
2041
|
+
return fromIstanbulMapData(this.coverageMap.toJSON());
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
function extractMetric(metric) {
|
|
2045
|
+
return {
|
|
2046
|
+
total: metric.total,
|
|
2047
|
+
covered: metric.covered,
|
|
2048
|
+
pct: metric.pct
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
function extractCompactCounts(coverageMapJSON, gitRoot) {
|
|
2052
|
+
const files = {};
|
|
2053
|
+
let fileCount = 0;
|
|
2054
|
+
for (const [filePath, fileCoverage] of Object.entries(coverageMapJSON)) {
|
|
2055
|
+
const normalizedPath = normalizePath(filePath, gitRoot);
|
|
2056
|
+
files[normalizedPath] = {
|
|
2057
|
+
s: fileCoverage.s,
|
|
2058
|
+
f: fileCoverage.f,
|
|
2059
|
+
b: fileCoverage.b,
|
|
2060
|
+
totals: {
|
|
2061
|
+
s: Object.keys(fileCoverage.statementMap || {}).length,
|
|
2062
|
+
f: Object.keys(fileCoverage.fnMap || {}).length,
|
|
2063
|
+
b: countBranchPaths(fileCoverage.branchMap || {})
|
|
2064
|
+
},
|
|
2065
|
+
shapeHash: computeShapeHash(fileCoverage)
|
|
2066
|
+
};
|
|
2067
|
+
fileCount++;
|
|
2068
|
+
}
|
|
2069
|
+
return { files, fileCount };
|
|
2070
|
+
}
|
|
2071
|
+
function countBranchPaths(branchMap) {
|
|
2072
|
+
let total = 0;
|
|
2073
|
+
for (const branch of Object.values(branchMap)) {
|
|
2074
|
+
total += (branch.locations || []).length;
|
|
2075
|
+
}
|
|
2076
|
+
return total;
|
|
2077
|
+
}
|
|
2078
|
+
function computeShapeHash(fileCoverage) {
|
|
2079
|
+
const branchMap = fileCoverage.branchMap || {};
|
|
2080
|
+
const shape = {
|
|
2081
|
+
s: Object.keys(fileCoverage.statementMap || {}).length,
|
|
2082
|
+
f: Object.keys(fileCoverage.fnMap || {}).length,
|
|
2083
|
+
b: countBranchPaths(branchMap),
|
|
2084
|
+
bp: Object.values(branchMap).map((b) => (b.locations || []).length)
|
|
2085
|
+
};
|
|
2086
|
+
return createHash("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 12);
|
|
2087
|
+
}
|
|
2088
|
+
async function generateIstanbulHtmlReport(coverageMerger, options) {
|
|
2089
|
+
await rm(options.outputDir, { recursive: true, force: true }).catch(() => {
|
|
2090
|
+
});
|
|
2091
|
+
await mkdir(options.outputDir, { recursive: true });
|
|
2092
|
+
const coverageMap = coverageMerger.getRawCoverageMap();
|
|
2093
|
+
const context = createContext({
|
|
2094
|
+
dir: options.outputDir,
|
|
2095
|
+
watermarks: {
|
|
2096
|
+
statements: [50, 80],
|
|
2097
|
+
functions: [50, 80],
|
|
2098
|
+
branches: [50, 80],
|
|
2099
|
+
lines: [50, 80]
|
|
2100
|
+
},
|
|
2101
|
+
coverageMap
|
|
2102
|
+
});
|
|
2103
|
+
const reporter = create("html", {
|
|
2104
|
+
skipEmpty: false,
|
|
2105
|
+
subdir: ""
|
|
2106
|
+
});
|
|
2107
|
+
reporter.execute(context);
|
|
2108
|
+
return `${options.outputDir}/index.html`;
|
|
2109
|
+
}
|
|
2110
|
+
var COVERAGE_EXTRACT_TIMEOUT_MS = 3e4;
|
|
2111
|
+
async function extractCoverageFromPage(page, timeoutMs = COVERAGE_EXTRACT_TIMEOUT_MS) {
|
|
2112
|
+
return Promise.race([
|
|
2113
|
+
page.evaluate(() => globalThis.__coverage__ ?? null),
|
|
2114
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
|
2115
|
+
]).catch(() => null);
|
|
2116
|
+
}
|
|
2117
|
+
async function attachCoverageToTestInfo(testInfo, coverage) {
|
|
2118
|
+
const fragment = {
|
|
2119
|
+
istanbul: coverage
|
|
2120
|
+
};
|
|
2121
|
+
await testInfo.attach("testdino-coverage", {
|
|
2122
|
+
body: JSON.stringify(fragment),
|
|
2123
|
+
contentType: "application/json"
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
var coverageFixtures = {
|
|
2127
|
+
_testdinoCoverage: [
|
|
2128
|
+
async ({ page }, use, testInfo) => {
|
|
2129
|
+
await use();
|
|
2130
|
+
const istanbulCoverage2 = await extractCoverageFromPage(page);
|
|
2131
|
+
if (istanbulCoverage2) {
|
|
2132
|
+
await attachCoverageToTestInfo(testInfo, istanbulCoverage2);
|
|
2133
|
+
}
|
|
2134
|
+
},
|
|
2135
|
+
{ auto: true }
|
|
2136
|
+
]
|
|
2137
|
+
};
|
|
2138
|
+
var test = test$1.extend(
|
|
2139
|
+
coverageFixtures
|
|
2140
|
+
);
|
|
2141
|
+
function stripAnsi(str) {
|
|
2142
|
+
return str.replace(/\u001b\[[0-9;]*m/g, "");
|
|
2143
|
+
}
|
|
2144
|
+
function pad(str, len) {
|
|
2145
|
+
const vLen = stripAnsi(str).length;
|
|
2146
|
+
return vLen >= len ? str : str + " ".repeat(len - vLen);
|
|
2147
|
+
}
|
|
2148
|
+
function padStart(str, len) {
|
|
2149
|
+
const vLen = stripAnsi(str).length;
|
|
2150
|
+
return vLen >= len ? str : " ".repeat(len - vLen) + str;
|
|
2151
|
+
}
|
|
2152
|
+
function formatDuration(ms) {
|
|
2153
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
2154
|
+
const seconds = ms / 1e3;
|
|
2155
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
2156
|
+
const minutes = Math.floor(seconds / 60);
|
|
2157
|
+
const remainingSeconds = (seconds % 60).toFixed(0);
|
|
2158
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
2159
|
+
}
|
|
2160
|
+
function shortenPath(filePath) {
|
|
2161
|
+
const markers = ["/src/", "/lib/", "/app/"];
|
|
2162
|
+
for (const marker of markers) {
|
|
2163
|
+
const idx = filePath.indexOf(marker);
|
|
2164
|
+
if (idx !== -1) return filePath.slice(idx + 1);
|
|
2165
|
+
}
|
|
2166
|
+
const parts = filePath.split("/");
|
|
2167
|
+
return parts.length > 2 ? parts.slice(-2).join("/") : filePath;
|
|
2168
|
+
}
|
|
2169
|
+
function colorPct(pct) {
|
|
2170
|
+
const color = pct >= 80 ? chalk.green : pct >= 50 ? chalk.yellow : chalk.red;
|
|
2171
|
+
return color(pct === 100 ? "100%" : `${pct.toFixed(1)}%`);
|
|
2172
|
+
}
|
|
2173
|
+
function printCoverageTable(event) {
|
|
2174
|
+
const { summary, files } = event;
|
|
2175
|
+
const row = (content) => ` ${chalk.dim("\u2502")} ${content}`;
|
|
2176
|
+
const nameW = 30;
|
|
2177
|
+
const colW = 10;
|
|
2178
|
+
console.log(row(`${chalk.bold("Coverage")} ${chalk.dim(`${files.length} files`)}`));
|
|
2179
|
+
console.log(row(""));
|
|
2180
|
+
console.log(
|
|
2181
|
+
row(
|
|
2182
|
+
` ${chalk.dim(`${pad("File", nameW)}${padStart("Stmts", colW)}${padStart("Branch", colW)}${padStart("Funcs", colW)}${padStart("Lines", colW)}`)}`
|
|
2183
|
+
)
|
|
2184
|
+
);
|
|
2185
|
+
for (const file of files) {
|
|
2186
|
+
const name = shortenPath(file.path);
|
|
2187
|
+
const short = name.length > nameW ? name.slice(0, nameW - 1) + "~" : name;
|
|
2188
|
+
console.log(
|
|
2189
|
+
row(
|
|
2190
|
+
` ${pad(short, nameW)}` + padStart(colorPct(file.statements.pct), colW) + padStart(colorPct(file.branches.pct), colW) + padStart(colorPct(file.functions.pct), colW) + padStart(colorPct(file.lines.pct), colW)
|
|
2191
|
+
)
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
console.log(row(` ${chalk.dim("\u2500".repeat(nameW + colW * 4))}`));
|
|
2195
|
+
console.log(
|
|
2196
|
+
row(
|
|
2197
|
+
` ${chalk.bold(pad("All files", nameW))}` + padStart(colorPct(summary.statements.pct), colW) + padStart(colorPct(summary.branches.pct), colW) + padStart(colorPct(summary.functions.pct), colW) + padStart(colorPct(summary.lines.pct), colW)
|
|
2198
|
+
)
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
function printRunSummary(result, streamingSuccess, data) {
|
|
2202
|
+
const W = 72;
|
|
2203
|
+
const topBorder = ` ${chalk.dim(`\u250C${"\u2500".repeat(W)}\u2510`)}`;
|
|
2204
|
+
const bottomBorder = ` ${chalk.dim(`\u2514${"\u2500".repeat(W)}\u2518`)}`;
|
|
2205
|
+
const divider = ` ${chalk.dim(`\u251C${"\u2500".repeat(W)}\u2524`)}`;
|
|
2206
|
+
const row = (content) => ` ${chalk.dim("\u2502")} ${content}`;
|
|
2207
|
+
console.log("");
|
|
2208
|
+
console.log(topBorder);
|
|
2209
|
+
console.log(row(chalk.bold("TestDino Run Summary")));
|
|
2210
|
+
console.log(divider);
|
|
2211
|
+
console.log(row(`${chalk.dim("Run")} ${data.runId}`));
|
|
2212
|
+
const git = data.runMetadata?.git;
|
|
2213
|
+
if (git?.branch || git?.commit?.hash) {
|
|
2214
|
+
const branch = git.branch ? chalk.cyan(git.branch) : "";
|
|
2215
|
+
const sha = git.commit?.hash ? chalk.dim(git.commit.hash.slice(0, 7)) : "";
|
|
2216
|
+
const sep = branch && sha ? ` ${chalk.dim("@")} ` : "";
|
|
2217
|
+
const msg = git.commit?.message ? ` ${chalk.dim(git.commit.message.split("\n")[0].slice(0, 50))}` : "";
|
|
2218
|
+
console.log(row(`${chalk.dim("Git")} ${branch}${sep}${sha}${msg}`));
|
|
2219
|
+
}
|
|
2220
|
+
console.log(divider);
|
|
2221
|
+
const statusColor = result.status === "passed" ? chalk.green : result.status === "failed" ? chalk.red : chalk.yellow;
|
|
2222
|
+
const statusLabel = result.status === "timedout" ? "timed out" : result.status;
|
|
2223
|
+
console.log(
|
|
2224
|
+
row(
|
|
2225
|
+
`${chalk.bold("Tests")} ${statusColor(statusLabel.toUpperCase())} ${chalk.dim(formatDuration(result.duration))}`
|
|
2226
|
+
)
|
|
2227
|
+
);
|
|
2228
|
+
const counts = [];
|
|
2229
|
+
if (data.testCounts.passed > 0) counts.push(chalk.green(`${data.testCounts.passed} passed`));
|
|
2230
|
+
if (data.testCounts.failed > 0) counts.push(chalk.red(`${data.testCounts.failed} failed`));
|
|
2231
|
+
if (data.testCounts.flaky > 0) counts.push(chalk.yellow(`${data.testCounts.flaky} flaky`));
|
|
2232
|
+
if (data.testCounts.skipped > 0) counts.push(chalk.dim(`${data.testCounts.skipped} skipped`));
|
|
2233
|
+
if (data.testCounts.timedOut > 0) counts.push(chalk.red(`${data.testCounts.timedOut} timed out`));
|
|
2234
|
+
if (data.testCounts.interrupted > 0) counts.push(chalk.yellow(`${data.testCounts.interrupted} interrupted`));
|
|
2235
|
+
const retriedStr = data.testCounts.retried > 0 ? ` ${chalk.dim(`(${data.testCounts.retried} retries)`)}` : "";
|
|
2236
|
+
console.log(row(` ${counts.join(chalk.dim(" \xB7 "))} ${chalk.dim(`of ${data.totalTests}`)}${retriedStr}`));
|
|
2237
|
+
console.log(divider);
|
|
2238
|
+
const shardStr = data.shardInfo ? `${data.shardInfo.current}/${data.shardInfo.total}` : "\u2014";
|
|
2239
|
+
console.log(
|
|
2240
|
+
row(
|
|
2241
|
+
`${pad(`${chalk.dim("Workers")} ${data.workerCount > 0 ? data.workerCount : "\u2014"}`, 28)}${pad(`${chalk.dim("Shard")} ${shardStr}`, 28)}${chalk.dim("Projects")} ${data.projectNames.size > 0 ? Array.from(data.projectNames).join(", ") : "\u2014"}`
|
|
2242
|
+
)
|
|
2243
|
+
);
|
|
2244
|
+
console.log(divider);
|
|
2245
|
+
const transport = data.useHttpFallback ? "HTTP" : "WebSocket";
|
|
2246
|
+
const streamIcon = streamingSuccess ? chalk.green("sent") : chalk.red("failed");
|
|
2247
|
+
console.log(row(`${chalk.bold("Stream")} ${streamIcon} ${chalk.dim(`via ${transport}`)}`));
|
|
2248
|
+
if (data.lastCoverageEvent) {
|
|
2249
|
+
console.log(divider);
|
|
2250
|
+
printCoverageTable(data.lastCoverageEvent);
|
|
2251
|
+
}
|
|
2252
|
+
console.log(bottomBorder);
|
|
2253
|
+
console.log("");
|
|
2254
|
+
}
|
|
1929
2255
|
var createReporterLog = (options) => ({
|
|
1930
2256
|
success: (msg) => console.log(`\u2705 TestDino: ${msg}`),
|
|
1931
2257
|
warn: (msg) => console.warn(`\u26A0\uFE0F TestDino: ${msg}`),
|
|
@@ -1935,12 +2261,15 @@ var createReporterLog = (options) => ({
|
|
|
1935
2261
|
if (options.debug) {
|
|
1936
2262
|
console.log(`\u{1F50D} TestDino: ${msg}`);
|
|
1937
2263
|
}
|
|
1938
|
-
}
|
|
2264
|
+
},
|
|
2265
|
+
printRunSummary,
|
|
2266
|
+
printCoverageTable
|
|
1939
2267
|
});
|
|
1940
2268
|
|
|
1941
2269
|
// src/reporter/index.ts
|
|
1942
2270
|
var MAX_CONSOLE_CHUNK_SIZE = 1e4;
|
|
1943
2271
|
var MAX_BUFFER_SIZE = 10;
|
|
2272
|
+
var COVERAGE_FILE_COUNT_WARNING = 500;
|
|
1944
2273
|
var TestdinoReporter = class {
|
|
1945
2274
|
config;
|
|
1946
2275
|
wsClient = null;
|
|
@@ -1971,11 +2300,32 @@ var TestdinoReporter = class {
|
|
|
1971
2300
|
pendingTestEndPromises = /* @__PURE__ */ new Set();
|
|
1972
2301
|
// Logger for consistent output
|
|
1973
2302
|
log;
|
|
2303
|
+
// Coverage collection
|
|
2304
|
+
coverageEnabled = false;
|
|
2305
|
+
coverageMerger = null;
|
|
2306
|
+
warnedCoverageDisconnect = false;
|
|
2307
|
+
coverageThresholdFailed = false;
|
|
2308
|
+
// Test result tracking for summary
|
|
2309
|
+
testCounts = { passed: 0, failed: 0, skipped: 0, timedOut: 0, interrupted: 0, flaky: 0, retried: 0 };
|
|
2310
|
+
totalTests = 0;
|
|
2311
|
+
lastCoverageEvent = null;
|
|
2312
|
+
// Detailed tracking for summary output
|
|
2313
|
+
projectNames = /* @__PURE__ */ new Set();
|
|
2314
|
+
runMetadata = null;
|
|
2315
|
+
workerCount = 0;
|
|
1974
2316
|
constructor(config = {}) {
|
|
1975
2317
|
const cliConfig = this.loadCliConfig();
|
|
1976
2318
|
this.config = { ...config, ...cliConfig };
|
|
1977
2319
|
this.runId = randomUUID();
|
|
1978
2320
|
this.log = createReporterLog({ debug: this.config.debug ?? false });
|
|
2321
|
+
this.coverageEnabled = this.config.coverage?.enabled ?? false;
|
|
2322
|
+
if (this.coverageEnabled) {
|
|
2323
|
+
this.coverageMerger = new CoverageMerger({
|
|
2324
|
+
include: this.config.coverage?.include,
|
|
2325
|
+
exclude: this.config.coverage?.exclude,
|
|
2326
|
+
onError: (msg) => this.log.warn(msg)
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
1979
2329
|
this.buffer = new EventBuffer({
|
|
1980
2330
|
maxSize: MAX_BUFFER_SIZE,
|
|
1981
2331
|
onFlush: async (events) => {
|
|
@@ -2017,6 +2367,9 @@ var TestdinoReporter = class {
|
|
|
2017
2367
|
if (cliConfig.artifacts !== void 0 && typeof cliConfig.artifacts === "boolean") {
|
|
2018
2368
|
mappedConfig.artifacts = cliConfig.artifacts;
|
|
2019
2369
|
}
|
|
2370
|
+
if (typeof cliConfig.coverage === "object" && cliConfig.coverage !== null) {
|
|
2371
|
+
mappedConfig.coverage = cliConfig.coverage;
|
|
2372
|
+
}
|
|
2020
2373
|
return mappedConfig;
|
|
2021
2374
|
} catch (error) {
|
|
2022
2375
|
if (isDebugEnabled()) {
|
|
@@ -2071,6 +2424,8 @@ var TestdinoReporter = class {
|
|
|
2071
2424
|
const serverUrl = this.getServerUrl();
|
|
2072
2425
|
try {
|
|
2073
2426
|
const metadata = await this.collectMetadata(config, suite);
|
|
2427
|
+
this.runMetadata = metadata;
|
|
2428
|
+
this.workerCount = config.workers;
|
|
2074
2429
|
this.httpClient = new HttpClient({ token, serverUrl });
|
|
2075
2430
|
const auth = await this.httpClient.authenticate();
|
|
2076
2431
|
this.sessionId = auth.sessionId;
|
|
@@ -2156,35 +2511,35 @@ var TestdinoReporter = class {
|
|
|
2156
2511
|
/**
|
|
2157
2512
|
* Called for each test before it starts
|
|
2158
2513
|
*/
|
|
2159
|
-
async onTestBegin(
|
|
2514
|
+
async onTestBegin(test2, result) {
|
|
2160
2515
|
if (!this.initPromise || this.initFailed) return;
|
|
2161
2516
|
const event = {
|
|
2162
2517
|
type: "test:begin",
|
|
2163
2518
|
runId: this.runId,
|
|
2164
2519
|
...this.getEventMetadata(),
|
|
2165
2520
|
// Test identification
|
|
2166
|
-
testId:
|
|
2167
|
-
title:
|
|
2168
|
-
titlePath:
|
|
2521
|
+
testId: test2.id,
|
|
2522
|
+
title: test2.title,
|
|
2523
|
+
titlePath: test2.titlePath(),
|
|
2169
2524
|
// Location information
|
|
2170
2525
|
location: {
|
|
2171
|
-
file:
|
|
2172
|
-
line:
|
|
2173
|
-
column:
|
|
2526
|
+
file: test2.location.file,
|
|
2527
|
+
line: test2.location.line,
|
|
2528
|
+
column: test2.location.column
|
|
2174
2529
|
},
|
|
2175
2530
|
// Test configuration
|
|
2176
|
-
tags:
|
|
2177
|
-
expectedStatus:
|
|
2178
|
-
timeout:
|
|
2179
|
-
retries:
|
|
2180
|
-
annotations: this.extractAnnotations(
|
|
2531
|
+
tags: test2.tags,
|
|
2532
|
+
expectedStatus: test2.expectedStatus,
|
|
2533
|
+
timeout: test2.timeout,
|
|
2534
|
+
retries: test2.retries,
|
|
2535
|
+
annotations: this.extractAnnotations(test2.annotations),
|
|
2181
2536
|
// Execution context
|
|
2182
2537
|
retry: result.retry,
|
|
2183
2538
|
workerIndex: result.workerIndex,
|
|
2184
2539
|
parallelIndex: result.parallelIndex,
|
|
2185
|
-
repeatEachIndex:
|
|
2540
|
+
repeatEachIndex: test2.repeatEachIndex,
|
|
2186
2541
|
// Hierarchy information
|
|
2187
|
-
parentSuite: this.extractParentSuite(
|
|
2542
|
+
parentSuite: this.extractParentSuite(test2.parent),
|
|
2188
2543
|
// Timing
|
|
2189
2544
|
startTime: result.startTime.getTime()
|
|
2190
2545
|
};
|
|
@@ -2193,15 +2548,15 @@ var TestdinoReporter = class {
|
|
|
2193
2548
|
/**
|
|
2194
2549
|
* Called when a test step begins
|
|
2195
2550
|
*/
|
|
2196
|
-
async onStepBegin(
|
|
2551
|
+
async onStepBegin(test2, result, step) {
|
|
2197
2552
|
if (!this.initPromise || this.initFailed) return;
|
|
2198
2553
|
const event = {
|
|
2199
2554
|
type: "step:begin",
|
|
2200
2555
|
runId: this.runId,
|
|
2201
2556
|
...this.getEventMetadata(),
|
|
2202
2557
|
// Step Identification
|
|
2203
|
-
testId:
|
|
2204
|
-
stepId: `${
|
|
2558
|
+
testId: test2.id,
|
|
2559
|
+
stepId: `${test2.id}-${step.titlePath().join("-")}`,
|
|
2205
2560
|
title: step.title,
|
|
2206
2561
|
titlePath: step.titlePath(),
|
|
2207
2562
|
// Step Classification
|
|
@@ -2229,7 +2584,7 @@ var TestdinoReporter = class {
|
|
|
2229
2584
|
/**
|
|
2230
2585
|
* Called when a test step ends
|
|
2231
2586
|
*/
|
|
2232
|
-
async onStepEnd(
|
|
2587
|
+
async onStepEnd(test2, result, step) {
|
|
2233
2588
|
if (!this.initPromise || this.initFailed) return;
|
|
2234
2589
|
const status = step.error ? "failed" : "passed";
|
|
2235
2590
|
const event = {
|
|
@@ -2237,8 +2592,8 @@ var TestdinoReporter = class {
|
|
|
2237
2592
|
runId: this.runId,
|
|
2238
2593
|
...this.getEventMetadata(),
|
|
2239
2594
|
// Step Identification
|
|
2240
|
-
testId:
|
|
2241
|
-
stepId: `${
|
|
2595
|
+
testId: test2.id,
|
|
2596
|
+
stepId: `${test2.id}-${step.titlePath().join("-")}`,
|
|
2242
2597
|
title: step.title,
|
|
2243
2598
|
titlePath: step.titlePath(),
|
|
2244
2599
|
// Timing
|
|
@@ -2265,9 +2620,46 @@ var TestdinoReporter = class {
|
|
|
2265
2620
|
* Called after each test.
|
|
2266
2621
|
* Playwright does not await onTestEnd promises—pending work is awaited in onEnd.
|
|
2267
2622
|
*/
|
|
2268
|
-
onTestEnd(
|
|
2623
|
+
onTestEnd(test2, result) {
|
|
2269
2624
|
if (!this.initPromise || this.initFailed) return;
|
|
2270
|
-
|
|
2625
|
+
if (!this.coverageEnabled && !this.warnedCoverageDisconnect) {
|
|
2626
|
+
const hasCoverageAttachment = result.attachments.some((a) => a.name === "testdino-coverage");
|
|
2627
|
+
if (hasCoverageAttachment) {
|
|
2628
|
+
this.log.warn(
|
|
2629
|
+
"Coverage data detected but coverage.enabled is false \u2014 set coverage: { enabled: true } to collect coverage"
|
|
2630
|
+
);
|
|
2631
|
+
this.warnedCoverageDisconnect = true;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
if (this.coverageEnabled && this.coverageMerger) {
|
|
2635
|
+
this.extractCoverageFromResult(test2, result, this.coverageMerger);
|
|
2636
|
+
}
|
|
2637
|
+
const projectName = this.getProjectName(test2);
|
|
2638
|
+
if (projectName) {
|
|
2639
|
+
this.projectNames.add(projectName);
|
|
2640
|
+
}
|
|
2641
|
+
if (result.retry > 0) {
|
|
2642
|
+
this.testCounts.retried++;
|
|
2643
|
+
}
|
|
2644
|
+
const isFinalAttempt = result.status === "passed" || result.retry === test2.retries;
|
|
2645
|
+
if (isFinalAttempt) {
|
|
2646
|
+
this.totalTests++;
|
|
2647
|
+
const outcome = test2.outcome();
|
|
2648
|
+
if (outcome === "flaky") {
|
|
2649
|
+
this.testCounts.flaky++;
|
|
2650
|
+
} else if (result.status === "passed") {
|
|
2651
|
+
this.testCounts.passed++;
|
|
2652
|
+
} else if (result.status === "failed") {
|
|
2653
|
+
this.testCounts.failed++;
|
|
2654
|
+
} else if (result.status === "skipped") {
|
|
2655
|
+
this.testCounts.skipped++;
|
|
2656
|
+
} else if (result.status === "timedOut") {
|
|
2657
|
+
this.testCounts.timedOut++;
|
|
2658
|
+
} else if (result.status === "interrupted") {
|
|
2659
|
+
this.testCounts.interrupted++;
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
const workPromise = this.processTestEnd(test2, result);
|
|
2271
2663
|
this.pendingTestEndPromises.add(workPromise);
|
|
2272
2664
|
workPromise.finally(() => {
|
|
2273
2665
|
this.pendingTestEndPromises.delete(workPromise);
|
|
@@ -2277,18 +2669,18 @@ var TestdinoReporter = class {
|
|
|
2277
2669
|
* Process test end event asynchronously
|
|
2278
2670
|
* Uploads attachments and adds test:end event to buffer
|
|
2279
2671
|
*/
|
|
2280
|
-
async processTestEnd(
|
|
2672
|
+
async processTestEnd(test2, result) {
|
|
2281
2673
|
try {
|
|
2282
|
-
const attachmentsWithUrls = await this.uploadAttachments(result.attachments,
|
|
2674
|
+
const attachmentsWithUrls = await this.uploadAttachments(result.attachments, test2.id);
|
|
2283
2675
|
const event = {
|
|
2284
2676
|
type: "test:end",
|
|
2285
2677
|
runId: this.runId,
|
|
2286
2678
|
...this.getEventMetadata(),
|
|
2287
2679
|
// Test Identification
|
|
2288
|
-
testId:
|
|
2680
|
+
testId: test2.id,
|
|
2289
2681
|
// Status Information
|
|
2290
2682
|
status: result.status,
|
|
2291
|
-
outcome:
|
|
2683
|
+
outcome: test2.outcome(),
|
|
2292
2684
|
// Timing
|
|
2293
2685
|
duration: result.duration,
|
|
2294
2686
|
// Execution Context
|
|
@@ -2339,6 +2731,43 @@ var TestdinoReporter = class {
|
|
|
2339
2731
|
this.log.debug(`Waiting for ${this.pendingTestEndPromises.size} pending test:end events...`);
|
|
2340
2732
|
await Promise.allSettled(Array.from(this.pendingTestEndPromises));
|
|
2341
2733
|
}
|
|
2734
|
+
if (this.coverageEnabled && this.coverageMerger?.hasCoverage) {
|
|
2735
|
+
try {
|
|
2736
|
+
if (this.projectNames.size > 1 && !this.config.coverage?.projects?.length) {
|
|
2737
|
+
this.log.warn(
|
|
2738
|
+
`Coverage collected from ${this.projectNames.size} projects (${Array.from(this.projectNames).join(", ")}). Set coverage.projects to a single project to avoid duplicate counts`
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
2741
|
+
if (this.config.coverage?.projects?.length) {
|
|
2742
|
+
const unmatched = this.config.coverage.projects.filter((p) => !this.projectNames.has(p));
|
|
2743
|
+
if (unmatched.length > 0) {
|
|
2744
|
+
this.log.warn(
|
|
2745
|
+
`coverage.projects contains unmatched values: ${unmatched.join(", ")}. Available projects: ${Array.from(this.projectNames).join(", ")}`
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
const coverageEvent = this.buildCoverageEvent(this.coverageMerger);
|
|
2750
|
+
await this.buffer.add(coverageEvent);
|
|
2751
|
+
this.lastCoverageEvent = coverageEvent;
|
|
2752
|
+
const thresholdFailures = this.checkCoverageThresholds(coverageEvent.summary);
|
|
2753
|
+
if (thresholdFailures.length > 0) {
|
|
2754
|
+
this.coverageThresholdFailed = true;
|
|
2755
|
+
}
|
|
2756
|
+
if (this.config.coverage?.localReport) {
|
|
2757
|
+
const outputDir = this.config.coverage.localReportDir || "./testdino-coverage";
|
|
2758
|
+
const reportPath = await generateIstanbulHtmlReport(this.coverageMerger, {
|
|
2759
|
+
outputDir
|
|
2760
|
+
});
|
|
2761
|
+
this.log.info(`Coverage Report: ${reportPath}`);
|
|
2762
|
+
}
|
|
2763
|
+
} catch (error) {
|
|
2764
|
+
this.log.warn(`Failed to build coverage event: ${error instanceof Error ? error.message : String(error)}`);
|
|
2765
|
+
}
|
|
2766
|
+
} else if (this.coverageEnabled && !this.coverageMerger?.hasCoverage) {
|
|
2767
|
+
this.log.warn(
|
|
2768
|
+
"Coverage enabled but no data was collected. Ensure your app is instrumented with Istanbul (babel-plugin-istanbul) and tests use { test } from @testdino/playwright"
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2342
2771
|
const event = {
|
|
2343
2772
|
type: "run:end",
|
|
2344
2773
|
runId: this.runId,
|
|
@@ -2389,6 +2818,28 @@ var TestdinoReporter = class {
|
|
|
2389
2818
|
this.log.error(`Failed to send run:end event: ${errorMessage}`);
|
|
2390
2819
|
}
|
|
2391
2820
|
}
|
|
2821
|
+
const summaryData = {
|
|
2822
|
+
runId: this.runId,
|
|
2823
|
+
runMetadata: this.runMetadata,
|
|
2824
|
+
testCounts: this.testCounts,
|
|
2825
|
+
totalTests: this.totalTests,
|
|
2826
|
+
workerCount: this.workerCount,
|
|
2827
|
+
projectNames: this.projectNames,
|
|
2828
|
+
shardInfo: this.shardInfo,
|
|
2829
|
+
useHttpFallback: this.useHttpFallback,
|
|
2830
|
+
lastCoverageEvent: this.lastCoverageEvent
|
|
2831
|
+
};
|
|
2832
|
+
this.log.printRunSummary(result, delivered, summaryData);
|
|
2833
|
+
if (this.coverageThresholdFailed && this.lastCoverageEvent) {
|
|
2834
|
+
const failures = this.checkCoverageThresholds(this.lastCoverageEvent.summary);
|
|
2835
|
+
this.log.error("Coverage thresholds not met:");
|
|
2836
|
+
for (const msg of failures) {
|
|
2837
|
+
this.log.error(` ${msg}`);
|
|
2838
|
+
}
|
|
2839
|
+
this.wsClient?.close();
|
|
2840
|
+
this.removeSignalHandlers();
|
|
2841
|
+
return { status: "failed" };
|
|
2842
|
+
}
|
|
2392
2843
|
this.wsClient?.close();
|
|
2393
2844
|
this.removeSignalHandlers();
|
|
2394
2845
|
}
|
|
@@ -2409,7 +2860,7 @@ var TestdinoReporter = class {
|
|
|
2409
2860
|
/**
|
|
2410
2861
|
* Called when standard output is produced in worker process
|
|
2411
2862
|
*/
|
|
2412
|
-
async onStdOut(chunk,
|
|
2863
|
+
async onStdOut(chunk, test2, result) {
|
|
2413
2864
|
if (!this.initPromise || this.initFailed) return;
|
|
2414
2865
|
const { text, truncated } = this.truncateChunk(chunk);
|
|
2415
2866
|
const event = {
|
|
@@ -2419,7 +2870,7 @@ var TestdinoReporter = class {
|
|
|
2419
2870
|
// Console Output
|
|
2420
2871
|
text,
|
|
2421
2872
|
// Test Association (optional)
|
|
2422
|
-
testId:
|
|
2873
|
+
testId: test2?.id,
|
|
2423
2874
|
retry: result?.retry,
|
|
2424
2875
|
// Truncation Indicator
|
|
2425
2876
|
truncated
|
|
@@ -2429,7 +2880,7 @@ var TestdinoReporter = class {
|
|
|
2429
2880
|
/**
|
|
2430
2881
|
* Called when standard error is produced in worker process
|
|
2431
2882
|
*/
|
|
2432
|
-
async onStdErr(chunk,
|
|
2883
|
+
async onStdErr(chunk, test2, result) {
|
|
2433
2884
|
if (!this.initPromise || this.initFailed) return;
|
|
2434
2885
|
const { text, truncated } = this.truncateChunk(chunk);
|
|
2435
2886
|
const event = {
|
|
@@ -2439,7 +2890,7 @@ var TestdinoReporter = class {
|
|
|
2439
2890
|
// Console Error Output
|
|
2440
2891
|
text,
|
|
2441
2892
|
// Test Association (optional)
|
|
2442
|
-
testId:
|
|
2893
|
+
testId: test2?.id,
|
|
2443
2894
|
retry: result?.retry,
|
|
2444
2895
|
// Truncation Indicator
|
|
2445
2896
|
truncated
|
|
@@ -2998,8 +3449,98 @@ var TestdinoReporter = class {
|
|
|
2998
3449
|
getBaseServerUrl(serverUrl) {
|
|
2999
3450
|
return serverUrl.replace(/\/api\/reporter$/, "");
|
|
3000
3451
|
}
|
|
3452
|
+
/**
|
|
3453
|
+
* Walk up the suite hierarchy to find the project name for a test.
|
|
3454
|
+
*/
|
|
3455
|
+
getProjectName(test2) {
|
|
3456
|
+
let suite = test2.parent;
|
|
3457
|
+
while (suite) {
|
|
3458
|
+
const project = suite.project();
|
|
3459
|
+
if (project) {
|
|
3460
|
+
return project.name || project.use?.defaultBrowserType || "default";
|
|
3461
|
+
}
|
|
3462
|
+
suite = suite.parent;
|
|
3463
|
+
}
|
|
3464
|
+
return void 0;
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Extract coverage fragment from test result attachments and merge incrementally.
|
|
3468
|
+
* The fixture attaches coverage as an in-memory JSON attachment named 'testdino-coverage'.
|
|
3469
|
+
*
|
|
3470
|
+
* Respects coverage.projects filter to avoid duplicate coverage from multiple browser projects.
|
|
3471
|
+
*/
|
|
3472
|
+
extractCoverageFromResult(test2, result, merger) {
|
|
3473
|
+
if (this.config.coverage?.projects) {
|
|
3474
|
+
const projectName = this.getProjectName(test2);
|
|
3475
|
+
if (projectName && !this.config.coverage.projects.includes(projectName)) {
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
const coverageAttachment = result.attachments.find((a) => a.name === "testdino-coverage");
|
|
3480
|
+
if (!coverageAttachment?.body) return;
|
|
3481
|
+
try {
|
|
3482
|
+
const fragment = JSON.parse(coverageAttachment.body.toString());
|
|
3483
|
+
merger.addFragment(fragment);
|
|
3484
|
+
} catch (error) {
|
|
3485
|
+
this.log.debug(
|
|
3486
|
+
`Malformed coverage attachment, skipping: ${error instanceof Error ? error.message : String(error)}`
|
|
3487
|
+
);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
/**
|
|
3491
|
+
* Build a coverage:data event from the merged coverage data.
|
|
3492
|
+
*
|
|
3493
|
+
* Non-sharded runs: summary + per-file metrics only (small payload).
|
|
3494
|
+
* Sharded runs: also includes compact hit counts for server-side cross-shard merging.
|
|
3495
|
+
*/
|
|
3496
|
+
buildCoverageEvent(merger) {
|
|
3497
|
+
const rootDir = this.runMetadata?.playwright?.rootDir || process.cwd();
|
|
3498
|
+
const summary = merger.computeSummary();
|
|
3499
|
+
const files = merger.computeFileCoverage(rootDir);
|
|
3500
|
+
const isSharded = !!this.shardInfo;
|
|
3501
|
+
if (files.length > COVERAGE_FILE_COUNT_WARNING) {
|
|
3502
|
+
this.log.warn(`Coverage includes ${files.length} files \u2014 consider using coverage.projects to reduce scope`);
|
|
3503
|
+
}
|
|
3504
|
+
const event = {
|
|
3505
|
+
type: "coverage:data",
|
|
3506
|
+
runId: this.runId,
|
|
3507
|
+
...this.getEventMetadata(),
|
|
3508
|
+
summary,
|
|
3509
|
+
files,
|
|
3510
|
+
metadata: {
|
|
3511
|
+
instrumentationType: "istanbul",
|
|
3512
|
+
fileCount: files.length,
|
|
3513
|
+
sharded: isSharded
|
|
3514
|
+
}
|
|
3515
|
+
};
|
|
3516
|
+
if (isSharded) {
|
|
3517
|
+
const coverageMapJSON = merger.toJSON();
|
|
3518
|
+
event.compactCounts = extractCompactCounts(coverageMapJSON, rootDir);
|
|
3519
|
+
event.shard = this.shardInfo;
|
|
3520
|
+
}
|
|
3521
|
+
return event;
|
|
3522
|
+
}
|
|
3523
|
+
/**
|
|
3524
|
+
* Check coverage against configured thresholds.
|
|
3525
|
+
* Returns an array of failure messages (empty if all thresholds pass).
|
|
3526
|
+
*/
|
|
3527
|
+
checkCoverageThresholds(summary) {
|
|
3528
|
+
const thresholds = this.config.coverage?.thresholds;
|
|
3529
|
+
if (!thresholds) return [];
|
|
3530
|
+
const failures = [];
|
|
3531
|
+
const check = (name, actual, threshold) => {
|
|
3532
|
+
if (threshold !== void 0 && actual < threshold) {
|
|
3533
|
+
failures.push(`${name}: ${actual.toFixed(2)}% < ${threshold}%`);
|
|
3534
|
+
}
|
|
3535
|
+
};
|
|
3536
|
+
check("Statements", summary.statements.pct, thresholds.statements);
|
|
3537
|
+
check("Branches", summary.branches.pct, thresholds.branches);
|
|
3538
|
+
check("Functions", summary.functions.pct, thresholds.functions);
|
|
3539
|
+
check("Lines", summary.lines.pct, thresholds.lines);
|
|
3540
|
+
return failures;
|
|
3541
|
+
}
|
|
3001
3542
|
};
|
|
3002
3543
|
|
|
3003
|
-
export { TestdinoReporter as default };
|
|
3544
|
+
export { coverageFixtures, TestdinoReporter as default, test };
|
|
3004
3545
|
//# sourceMappingURL=index.mjs.map
|
|
3005
3546
|
//# sourceMappingURL=index.mjs.map
|