@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.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
3
5
|
var crypto = require('crypto');
|
|
4
6
|
var fs = require('fs');
|
|
5
7
|
var WebSocket = require('ws');
|
|
6
8
|
var axios = require('axios');
|
|
9
|
+
var path = require('path');
|
|
7
10
|
var promises = require('fs/promises');
|
|
8
11
|
var execa = require('execa');
|
|
9
12
|
var os = require('os');
|
|
10
13
|
var process$1 = require('process');
|
|
11
|
-
var
|
|
14
|
+
var istanbulCoverage = require('istanbul-lib-coverage');
|
|
15
|
+
var istanbulLibReport = require('istanbul-lib-report');
|
|
16
|
+
var istanbulReports = require('istanbul-reports');
|
|
17
|
+
var test$1 = require('@playwright/test');
|
|
18
|
+
var chalk = require('chalk');
|
|
12
19
|
|
|
13
20
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
21
|
|
|
15
22
|
var WebSocket__default = /*#__PURE__*/_interopDefault(WebSocket);
|
|
16
23
|
var axios__default = /*#__PURE__*/_interopDefault(axios);
|
|
24
|
+
var istanbulCoverage__default = /*#__PURE__*/_interopDefault(istanbulCoverage);
|
|
25
|
+
var chalk__default = /*#__PURE__*/_interopDefault(chalk);
|
|
17
26
|
|
|
18
27
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
19
28
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -373,8 +382,12 @@ var WebSocketClient = class {
|
|
|
373
382
|
}
|
|
374
383
|
}
|
|
375
384
|
};
|
|
376
|
-
|
|
377
|
-
|
|
385
|
+
function normalizePath(filePath, rootDir) {
|
|
386
|
+
if (rootDir && filePath.startsWith(rootDir)) {
|
|
387
|
+
return path.relative(rootDir, filePath);
|
|
388
|
+
}
|
|
389
|
+
return filePath;
|
|
390
|
+
}
|
|
378
391
|
function sleep(ms) {
|
|
379
392
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
380
393
|
}
|
|
@@ -1521,7 +1534,7 @@ var PlaywrightMetadataCollector = class extends BaseMetadataCollector {
|
|
|
1521
1534
|
const skeletonSuite = {
|
|
1522
1535
|
title: childSuite.title,
|
|
1523
1536
|
type: childSuite.type === "file" ? "file" : "describe",
|
|
1524
|
-
tests: childSuite.tests.map((
|
|
1537
|
+
tests: childSuite.tests.map((test2) => this.buildSkeletonTest(test2))
|
|
1525
1538
|
};
|
|
1526
1539
|
if (childSuite.type === "file" && childSuite.location) {
|
|
1527
1540
|
skeletonSuite.file = childSuite.location.file;
|
|
@@ -1543,24 +1556,24 @@ var PlaywrightMetadataCollector = class extends BaseMetadataCollector {
|
|
|
1543
1556
|
/**
|
|
1544
1557
|
* Build skeleton test from TestCase
|
|
1545
1558
|
*/
|
|
1546
|
-
buildSkeletonTest(
|
|
1559
|
+
buildSkeletonTest(test2) {
|
|
1547
1560
|
const skeletonTest = {
|
|
1548
|
-
testId:
|
|
1549
|
-
title:
|
|
1561
|
+
testId: test2.id,
|
|
1562
|
+
title: test2.title,
|
|
1550
1563
|
location: {
|
|
1551
|
-
file:
|
|
1552
|
-
line:
|
|
1553
|
-
column:
|
|
1564
|
+
file: test2.location.file,
|
|
1565
|
+
line: test2.location.line,
|
|
1566
|
+
column: test2.location.column
|
|
1554
1567
|
}
|
|
1555
1568
|
};
|
|
1556
|
-
if (
|
|
1557
|
-
skeletonTest.tags =
|
|
1569
|
+
if (test2.tags && test2.tags.length > 0) {
|
|
1570
|
+
skeletonTest.tags = test2.tags;
|
|
1558
1571
|
}
|
|
1559
|
-
if (
|
|
1560
|
-
skeletonTest.expectedStatus =
|
|
1572
|
+
if (test2.expectedStatus) {
|
|
1573
|
+
skeletonTest.expectedStatus = test2.expectedStatus;
|
|
1561
1574
|
}
|
|
1562
|
-
if (
|
|
1563
|
-
skeletonTest.annotations =
|
|
1575
|
+
if (test2.annotations && test2.annotations.length > 0) {
|
|
1576
|
+
skeletonTest.annotations = test2.annotations.map((ann) => ({
|
|
1564
1577
|
type: ann.type,
|
|
1565
1578
|
description: ann.description
|
|
1566
1579
|
}));
|
|
@@ -1931,8 +1944,324 @@ var ArtifactUploader = class {
|
|
|
1931
1944
|
return this.sasToken.uniqueId;
|
|
1932
1945
|
}
|
|
1933
1946
|
};
|
|
1934
|
-
|
|
1935
|
-
|
|
1947
|
+
function toIstanbulMapData(map) {
|
|
1948
|
+
return map;
|
|
1949
|
+
}
|
|
1950
|
+
function fromIstanbulMapData(data) {
|
|
1951
|
+
return data;
|
|
1952
|
+
}
|
|
1953
|
+
var CoverageMerger = class {
|
|
1954
|
+
coverageMap = istanbulCoverage__default.default.createCoverageMap({});
|
|
1955
|
+
hasData = false;
|
|
1956
|
+
includePatterns;
|
|
1957
|
+
excludePatterns;
|
|
1958
|
+
onError;
|
|
1959
|
+
constructor(options) {
|
|
1960
|
+
this.includePatterns = options?.include;
|
|
1961
|
+
this.excludePatterns = options?.exclude;
|
|
1962
|
+
this.onError = options?.onError;
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Merge a coverage fragment from a completed test.
|
|
1966
|
+
* The fragment is merged directly and can be GC'd immediately.
|
|
1967
|
+
*/
|
|
1968
|
+
addFragment(fragment) {
|
|
1969
|
+
if (!fragment.istanbul) return;
|
|
1970
|
+
try {
|
|
1971
|
+
const filtered = this.filterCoverageMap(fragment.istanbul);
|
|
1972
|
+
this.coverageMap.merge(toIstanbulMapData(filtered));
|
|
1973
|
+
if (this.coverageMap.files().length > 0) {
|
|
1974
|
+
this.hasData = true;
|
|
1975
|
+
}
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
const msg = `[TestDino] Failed to merge coverage fragment: ${error instanceof Error ? error.message : String(error)}`;
|
|
1978
|
+
if (this.onError) {
|
|
1979
|
+
this.onError(msg);
|
|
1980
|
+
} else {
|
|
1981
|
+
console.warn(msg);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Filter files from a coverage map based on include/exclude patterns.
|
|
1987
|
+
*/
|
|
1988
|
+
filterCoverageMap(coverageMap) {
|
|
1989
|
+
const hasInclude = this.includePatterns && this.includePatterns.length > 0;
|
|
1990
|
+
const hasExclude = this.excludePatterns && this.excludePatterns.length > 0;
|
|
1991
|
+
if (!hasInclude && !hasExclude) return coverageMap;
|
|
1992
|
+
const filtered = {};
|
|
1993
|
+
for (const [filePath, fileCoverage] of Object.entries(coverageMap)) {
|
|
1994
|
+
if (hasExclude && this.excludePatterns.some((pattern) => filePath.includes(pattern))) {
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
if (hasInclude && !this.includePatterns.some((pattern) => filePath.includes(pattern))) {
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
filtered[filePath] = fileCoverage;
|
|
2001
|
+
}
|
|
2002
|
+
return filtered;
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Whether any coverage data has been collected.
|
|
2006
|
+
*/
|
|
2007
|
+
get hasCoverage() {
|
|
2008
|
+
return this.hasData;
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Compute aggregate summary metrics from the merged coverage map.
|
|
2012
|
+
*/
|
|
2013
|
+
computeSummary() {
|
|
2014
|
+
const globalSummary = this.coverageMap.getCoverageSummary();
|
|
2015
|
+
return {
|
|
2016
|
+
lines: extractMetric(globalSummary.lines),
|
|
2017
|
+
branches: extractMetric(globalSummary.branches),
|
|
2018
|
+
functions: extractMetric(globalSummary.functions),
|
|
2019
|
+
statements: extractMetric(globalSummary.statements)
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Compute per-file coverage metrics.
|
|
2024
|
+
* Normalizes file paths to be relative to git root.
|
|
2025
|
+
*/
|
|
2026
|
+
computeFileCoverage(gitRoot) {
|
|
2027
|
+
const root = gitRoot || process.cwd();
|
|
2028
|
+
return this.coverageMap.files().map((filePath) => {
|
|
2029
|
+
const fileCoverage = this.coverageMap.fileCoverageFor(filePath);
|
|
2030
|
+
const fileSummary = fileCoverage.toSummary();
|
|
2031
|
+
const normalizedPath = normalizePath(filePath, root);
|
|
2032
|
+
return {
|
|
2033
|
+
path: normalizedPath,
|
|
2034
|
+
lines: extractMetric(fileSummary.lines),
|
|
2035
|
+
branches: extractMetric(fileSummary.branches),
|
|
2036
|
+
functions: extractMetric(fileSummary.functions),
|
|
2037
|
+
statements: extractMetric(fileSummary.statements)
|
|
2038
|
+
};
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Get the raw merged coverage map (for local report generation or compact extraction).
|
|
2043
|
+
*/
|
|
2044
|
+
getRawCoverageMap() {
|
|
2045
|
+
return this.coverageMap;
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Get the merged coverage map as a plain JSON object.
|
|
2049
|
+
*/
|
|
2050
|
+
toJSON() {
|
|
2051
|
+
return fromIstanbulMapData(this.coverageMap.toJSON());
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
function extractMetric(metric) {
|
|
2055
|
+
return {
|
|
2056
|
+
total: metric.total,
|
|
2057
|
+
covered: metric.covered,
|
|
2058
|
+
pct: metric.pct
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
function extractCompactCounts(coverageMapJSON, gitRoot) {
|
|
2062
|
+
const files = {};
|
|
2063
|
+
let fileCount = 0;
|
|
2064
|
+
for (const [filePath, fileCoverage] of Object.entries(coverageMapJSON)) {
|
|
2065
|
+
const normalizedPath = normalizePath(filePath, gitRoot);
|
|
2066
|
+
files[normalizedPath] = {
|
|
2067
|
+
s: fileCoverage.s,
|
|
2068
|
+
f: fileCoverage.f,
|
|
2069
|
+
b: fileCoverage.b,
|
|
2070
|
+
totals: {
|
|
2071
|
+
s: Object.keys(fileCoverage.statementMap || {}).length,
|
|
2072
|
+
f: Object.keys(fileCoverage.fnMap || {}).length,
|
|
2073
|
+
b: countBranchPaths(fileCoverage.branchMap || {})
|
|
2074
|
+
},
|
|
2075
|
+
shapeHash: computeShapeHash(fileCoverage)
|
|
2076
|
+
};
|
|
2077
|
+
fileCount++;
|
|
2078
|
+
}
|
|
2079
|
+
return { files, fileCount };
|
|
2080
|
+
}
|
|
2081
|
+
function countBranchPaths(branchMap) {
|
|
2082
|
+
let total = 0;
|
|
2083
|
+
for (const branch of Object.values(branchMap)) {
|
|
2084
|
+
total += (branch.locations || []).length;
|
|
2085
|
+
}
|
|
2086
|
+
return total;
|
|
2087
|
+
}
|
|
2088
|
+
function computeShapeHash(fileCoverage) {
|
|
2089
|
+
const branchMap = fileCoverage.branchMap || {};
|
|
2090
|
+
const shape = {
|
|
2091
|
+
s: Object.keys(fileCoverage.statementMap || {}).length,
|
|
2092
|
+
f: Object.keys(fileCoverage.fnMap || {}).length,
|
|
2093
|
+
b: countBranchPaths(branchMap),
|
|
2094
|
+
bp: Object.values(branchMap).map((b) => (b.locations || []).length)
|
|
2095
|
+
};
|
|
2096
|
+
return crypto.createHash("sha256").update(JSON.stringify(shape)).digest("hex").slice(0, 12);
|
|
2097
|
+
}
|
|
2098
|
+
async function generateIstanbulHtmlReport(coverageMerger, options) {
|
|
2099
|
+
await promises.rm(options.outputDir, { recursive: true, force: true }).catch(() => {
|
|
2100
|
+
});
|
|
2101
|
+
await promises.mkdir(options.outputDir, { recursive: true });
|
|
2102
|
+
const coverageMap = coverageMerger.getRawCoverageMap();
|
|
2103
|
+
const context = istanbulLibReport.createContext({
|
|
2104
|
+
dir: options.outputDir,
|
|
2105
|
+
watermarks: {
|
|
2106
|
+
statements: [50, 80],
|
|
2107
|
+
functions: [50, 80],
|
|
2108
|
+
branches: [50, 80],
|
|
2109
|
+
lines: [50, 80]
|
|
2110
|
+
},
|
|
2111
|
+
coverageMap
|
|
2112
|
+
});
|
|
2113
|
+
const reporter = istanbulReports.create("html", {
|
|
2114
|
+
skipEmpty: false,
|
|
2115
|
+
subdir: ""
|
|
2116
|
+
});
|
|
2117
|
+
reporter.execute(context);
|
|
2118
|
+
return `${options.outputDir}/index.html`;
|
|
2119
|
+
}
|
|
2120
|
+
var COVERAGE_EXTRACT_TIMEOUT_MS = 3e4;
|
|
2121
|
+
async function extractCoverageFromPage(page, timeoutMs = COVERAGE_EXTRACT_TIMEOUT_MS) {
|
|
2122
|
+
return Promise.race([
|
|
2123
|
+
page.evaluate(() => globalThis.__coverage__ ?? null),
|
|
2124
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
|
2125
|
+
]).catch(() => null);
|
|
2126
|
+
}
|
|
2127
|
+
async function attachCoverageToTestInfo(testInfo, coverage) {
|
|
2128
|
+
const fragment = {
|
|
2129
|
+
istanbul: coverage
|
|
2130
|
+
};
|
|
2131
|
+
await testInfo.attach("testdino-coverage", {
|
|
2132
|
+
body: JSON.stringify(fragment),
|
|
2133
|
+
contentType: "application/json"
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
var coverageFixtures = {
|
|
2137
|
+
_testdinoCoverage: [
|
|
2138
|
+
async ({ page }, use, testInfo) => {
|
|
2139
|
+
await use();
|
|
2140
|
+
const istanbulCoverage2 = await extractCoverageFromPage(page);
|
|
2141
|
+
if (istanbulCoverage2) {
|
|
2142
|
+
await attachCoverageToTestInfo(testInfo, istanbulCoverage2);
|
|
2143
|
+
}
|
|
2144
|
+
},
|
|
2145
|
+
{ auto: true }
|
|
2146
|
+
]
|
|
2147
|
+
};
|
|
2148
|
+
var test = test$1.test.extend(
|
|
2149
|
+
coverageFixtures
|
|
2150
|
+
);
|
|
2151
|
+
function stripAnsi(str) {
|
|
2152
|
+
return str.replace(/\u001b\[[0-9;]*m/g, "");
|
|
2153
|
+
}
|
|
2154
|
+
function pad(str, len) {
|
|
2155
|
+
const vLen = stripAnsi(str).length;
|
|
2156
|
+
return vLen >= len ? str : str + " ".repeat(len - vLen);
|
|
2157
|
+
}
|
|
2158
|
+
function padStart(str, len) {
|
|
2159
|
+
const vLen = stripAnsi(str).length;
|
|
2160
|
+
return vLen >= len ? str : " ".repeat(len - vLen) + str;
|
|
2161
|
+
}
|
|
2162
|
+
function formatDuration(ms) {
|
|
2163
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
2164
|
+
const seconds = ms / 1e3;
|
|
2165
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
2166
|
+
const minutes = Math.floor(seconds / 60);
|
|
2167
|
+
const remainingSeconds = (seconds % 60).toFixed(0);
|
|
2168
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
2169
|
+
}
|
|
2170
|
+
function shortenPath(filePath) {
|
|
2171
|
+
const markers = ["/src/", "/lib/", "/app/"];
|
|
2172
|
+
for (const marker of markers) {
|
|
2173
|
+
const idx = filePath.indexOf(marker);
|
|
2174
|
+
if (idx !== -1) return filePath.slice(idx + 1);
|
|
2175
|
+
}
|
|
2176
|
+
const parts = filePath.split("/");
|
|
2177
|
+
return parts.length > 2 ? parts.slice(-2).join("/") : filePath;
|
|
2178
|
+
}
|
|
2179
|
+
function colorPct(pct) {
|
|
2180
|
+
const color = pct >= 80 ? chalk__default.default.green : pct >= 50 ? chalk__default.default.yellow : chalk__default.default.red;
|
|
2181
|
+
return color(pct === 100 ? "100%" : `${pct.toFixed(1)}%`);
|
|
2182
|
+
}
|
|
2183
|
+
function printCoverageTable(event) {
|
|
2184
|
+
const { summary, files } = event;
|
|
2185
|
+
const row = (content) => ` ${chalk__default.default.dim("\u2502")} ${content}`;
|
|
2186
|
+
const nameW = 30;
|
|
2187
|
+
const colW = 10;
|
|
2188
|
+
console.log(row(`${chalk__default.default.bold("Coverage")} ${chalk__default.default.dim(`${files.length} files`)}`));
|
|
2189
|
+
console.log(row(""));
|
|
2190
|
+
console.log(
|
|
2191
|
+
row(
|
|
2192
|
+
` ${chalk__default.default.dim(`${pad("File", nameW)}${padStart("Stmts", colW)}${padStart("Branch", colW)}${padStart("Funcs", colW)}${padStart("Lines", colW)}`)}`
|
|
2193
|
+
)
|
|
2194
|
+
);
|
|
2195
|
+
for (const file of files) {
|
|
2196
|
+
const name = shortenPath(file.path);
|
|
2197
|
+
const short = name.length > nameW ? name.slice(0, nameW - 1) + "~" : name;
|
|
2198
|
+
console.log(
|
|
2199
|
+
row(
|
|
2200
|
+
` ${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)
|
|
2201
|
+
)
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
console.log(row(` ${chalk__default.default.dim("\u2500".repeat(nameW + colW * 4))}`));
|
|
2205
|
+
console.log(
|
|
2206
|
+
row(
|
|
2207
|
+
` ${chalk__default.default.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)
|
|
2208
|
+
)
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
function printRunSummary(result, streamingSuccess, data) {
|
|
2212
|
+
const W = 72;
|
|
2213
|
+
const topBorder = ` ${chalk__default.default.dim(`\u250C${"\u2500".repeat(W)}\u2510`)}`;
|
|
2214
|
+
const bottomBorder = ` ${chalk__default.default.dim(`\u2514${"\u2500".repeat(W)}\u2518`)}`;
|
|
2215
|
+
const divider = ` ${chalk__default.default.dim(`\u251C${"\u2500".repeat(W)}\u2524`)}`;
|
|
2216
|
+
const row = (content) => ` ${chalk__default.default.dim("\u2502")} ${content}`;
|
|
2217
|
+
console.log("");
|
|
2218
|
+
console.log(topBorder);
|
|
2219
|
+
console.log(row(chalk__default.default.bold("TestDino Run Summary")));
|
|
2220
|
+
console.log(divider);
|
|
2221
|
+
console.log(row(`${chalk__default.default.dim("Run")} ${data.runId}`));
|
|
2222
|
+
const git = data.runMetadata?.git;
|
|
2223
|
+
if (git?.branch || git?.commit?.hash) {
|
|
2224
|
+
const branch = git.branch ? chalk__default.default.cyan(git.branch) : "";
|
|
2225
|
+
const sha = git.commit?.hash ? chalk__default.default.dim(git.commit.hash.slice(0, 7)) : "";
|
|
2226
|
+
const sep = branch && sha ? ` ${chalk__default.default.dim("@")} ` : "";
|
|
2227
|
+
const msg = git.commit?.message ? ` ${chalk__default.default.dim(git.commit.message.split("\n")[0].slice(0, 50))}` : "";
|
|
2228
|
+
console.log(row(`${chalk__default.default.dim("Git")} ${branch}${sep}${sha}${msg}`));
|
|
2229
|
+
}
|
|
2230
|
+
console.log(divider);
|
|
2231
|
+
const statusColor = result.status === "passed" ? chalk__default.default.green : result.status === "failed" ? chalk__default.default.red : chalk__default.default.yellow;
|
|
2232
|
+
const statusLabel = result.status === "timedout" ? "timed out" : result.status;
|
|
2233
|
+
console.log(
|
|
2234
|
+
row(
|
|
2235
|
+
`${chalk__default.default.bold("Tests")} ${statusColor(statusLabel.toUpperCase())} ${chalk__default.default.dim(formatDuration(result.duration))}`
|
|
2236
|
+
)
|
|
2237
|
+
);
|
|
2238
|
+
const counts = [];
|
|
2239
|
+
if (data.testCounts.passed > 0) counts.push(chalk__default.default.green(`${data.testCounts.passed} passed`));
|
|
2240
|
+
if (data.testCounts.failed > 0) counts.push(chalk__default.default.red(`${data.testCounts.failed} failed`));
|
|
2241
|
+
if (data.testCounts.flaky > 0) counts.push(chalk__default.default.yellow(`${data.testCounts.flaky} flaky`));
|
|
2242
|
+
if (data.testCounts.skipped > 0) counts.push(chalk__default.default.dim(`${data.testCounts.skipped} skipped`));
|
|
2243
|
+
if (data.testCounts.timedOut > 0) counts.push(chalk__default.default.red(`${data.testCounts.timedOut} timed out`));
|
|
2244
|
+
if (data.testCounts.interrupted > 0) counts.push(chalk__default.default.yellow(`${data.testCounts.interrupted} interrupted`));
|
|
2245
|
+
const retriedStr = data.testCounts.retried > 0 ? ` ${chalk__default.default.dim(`(${data.testCounts.retried} retries)`)}` : "";
|
|
2246
|
+
console.log(row(` ${counts.join(chalk__default.default.dim(" \xB7 "))} ${chalk__default.default.dim(`of ${data.totalTests}`)}${retriedStr}`));
|
|
2247
|
+
console.log(divider);
|
|
2248
|
+
const shardStr = data.shardInfo ? `${data.shardInfo.current}/${data.shardInfo.total}` : "\u2014";
|
|
2249
|
+
console.log(
|
|
2250
|
+
row(
|
|
2251
|
+
`${pad(`${chalk__default.default.dim("Workers")} ${data.workerCount > 0 ? data.workerCount : "\u2014"}`, 28)}${pad(`${chalk__default.default.dim("Shard")} ${shardStr}`, 28)}${chalk__default.default.dim("Projects")} ${data.projectNames.size > 0 ? Array.from(data.projectNames).join(", ") : "\u2014"}`
|
|
2252
|
+
)
|
|
2253
|
+
);
|
|
2254
|
+
console.log(divider);
|
|
2255
|
+
const transport = data.useHttpFallback ? "HTTP" : "WebSocket";
|
|
2256
|
+
const streamIcon = streamingSuccess ? chalk__default.default.green("sent") : chalk__default.default.red("failed");
|
|
2257
|
+
console.log(row(`${chalk__default.default.bold("Stream")} ${streamIcon} ${chalk__default.default.dim(`via ${transport}`)}`));
|
|
2258
|
+
if (data.lastCoverageEvent) {
|
|
2259
|
+
console.log(divider);
|
|
2260
|
+
printCoverageTable(data.lastCoverageEvent);
|
|
2261
|
+
}
|
|
2262
|
+
console.log(bottomBorder);
|
|
2263
|
+
console.log("");
|
|
2264
|
+
}
|
|
1936
2265
|
var createReporterLog = (options) => ({
|
|
1937
2266
|
success: (msg) => console.log(`\u2705 TestDino: ${msg}`),
|
|
1938
2267
|
warn: (msg) => console.warn(`\u26A0\uFE0F TestDino: ${msg}`),
|
|
@@ -1942,12 +2271,15 @@ var createReporterLog = (options) => ({
|
|
|
1942
2271
|
if (options.debug) {
|
|
1943
2272
|
console.log(`\u{1F50D} TestDino: ${msg}`);
|
|
1944
2273
|
}
|
|
1945
|
-
}
|
|
2274
|
+
},
|
|
2275
|
+
printRunSummary,
|
|
2276
|
+
printCoverageTable
|
|
1946
2277
|
});
|
|
1947
2278
|
|
|
1948
2279
|
// src/reporter/index.ts
|
|
1949
2280
|
var MAX_CONSOLE_CHUNK_SIZE = 1e4;
|
|
1950
2281
|
var MAX_BUFFER_SIZE = 10;
|
|
2282
|
+
var COVERAGE_FILE_COUNT_WARNING = 500;
|
|
1951
2283
|
var TestdinoReporter = class {
|
|
1952
2284
|
config;
|
|
1953
2285
|
wsClient = null;
|
|
@@ -1978,11 +2310,32 @@ var TestdinoReporter = class {
|
|
|
1978
2310
|
pendingTestEndPromises = /* @__PURE__ */ new Set();
|
|
1979
2311
|
// Logger for consistent output
|
|
1980
2312
|
log;
|
|
2313
|
+
// Coverage collection
|
|
2314
|
+
coverageEnabled = false;
|
|
2315
|
+
coverageMerger = null;
|
|
2316
|
+
warnedCoverageDisconnect = false;
|
|
2317
|
+
coverageThresholdFailed = false;
|
|
2318
|
+
// Test result tracking for summary
|
|
2319
|
+
testCounts = { passed: 0, failed: 0, skipped: 0, timedOut: 0, interrupted: 0, flaky: 0, retried: 0 };
|
|
2320
|
+
totalTests = 0;
|
|
2321
|
+
lastCoverageEvent = null;
|
|
2322
|
+
// Detailed tracking for summary output
|
|
2323
|
+
projectNames = /* @__PURE__ */ new Set();
|
|
2324
|
+
runMetadata = null;
|
|
2325
|
+
workerCount = 0;
|
|
1981
2326
|
constructor(config = {}) {
|
|
1982
2327
|
const cliConfig = this.loadCliConfig();
|
|
1983
2328
|
this.config = { ...config, ...cliConfig };
|
|
1984
2329
|
this.runId = crypto.randomUUID();
|
|
1985
2330
|
this.log = createReporterLog({ debug: this.config.debug ?? false });
|
|
2331
|
+
this.coverageEnabled = this.config.coverage?.enabled ?? false;
|
|
2332
|
+
if (this.coverageEnabled) {
|
|
2333
|
+
this.coverageMerger = new CoverageMerger({
|
|
2334
|
+
include: this.config.coverage?.include,
|
|
2335
|
+
exclude: this.config.coverage?.exclude,
|
|
2336
|
+
onError: (msg) => this.log.warn(msg)
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
1986
2339
|
this.buffer = new EventBuffer({
|
|
1987
2340
|
maxSize: MAX_BUFFER_SIZE,
|
|
1988
2341
|
onFlush: async (events) => {
|
|
@@ -2024,6 +2377,9 @@ var TestdinoReporter = class {
|
|
|
2024
2377
|
if (cliConfig.artifacts !== void 0 && typeof cliConfig.artifacts === "boolean") {
|
|
2025
2378
|
mappedConfig.artifacts = cliConfig.artifacts;
|
|
2026
2379
|
}
|
|
2380
|
+
if (typeof cliConfig.coverage === "object" && cliConfig.coverage !== null) {
|
|
2381
|
+
mappedConfig.coverage = cliConfig.coverage;
|
|
2382
|
+
}
|
|
2027
2383
|
return mappedConfig;
|
|
2028
2384
|
} catch (error) {
|
|
2029
2385
|
if (isDebugEnabled()) {
|
|
@@ -2078,6 +2434,8 @@ var TestdinoReporter = class {
|
|
|
2078
2434
|
const serverUrl = this.getServerUrl();
|
|
2079
2435
|
try {
|
|
2080
2436
|
const metadata = await this.collectMetadata(config, suite);
|
|
2437
|
+
this.runMetadata = metadata;
|
|
2438
|
+
this.workerCount = config.workers;
|
|
2081
2439
|
this.httpClient = new HttpClient({ token, serverUrl });
|
|
2082
2440
|
const auth = await this.httpClient.authenticate();
|
|
2083
2441
|
this.sessionId = auth.sessionId;
|
|
@@ -2163,35 +2521,35 @@ var TestdinoReporter = class {
|
|
|
2163
2521
|
/**
|
|
2164
2522
|
* Called for each test before it starts
|
|
2165
2523
|
*/
|
|
2166
|
-
async onTestBegin(
|
|
2524
|
+
async onTestBegin(test2, result) {
|
|
2167
2525
|
if (!this.initPromise || this.initFailed) return;
|
|
2168
2526
|
const event = {
|
|
2169
2527
|
type: "test:begin",
|
|
2170
2528
|
runId: this.runId,
|
|
2171
2529
|
...this.getEventMetadata(),
|
|
2172
2530
|
// Test identification
|
|
2173
|
-
testId:
|
|
2174
|
-
title:
|
|
2175
|
-
titlePath:
|
|
2531
|
+
testId: test2.id,
|
|
2532
|
+
title: test2.title,
|
|
2533
|
+
titlePath: test2.titlePath(),
|
|
2176
2534
|
// Location information
|
|
2177
2535
|
location: {
|
|
2178
|
-
file:
|
|
2179
|
-
line:
|
|
2180
|
-
column:
|
|
2536
|
+
file: test2.location.file,
|
|
2537
|
+
line: test2.location.line,
|
|
2538
|
+
column: test2.location.column
|
|
2181
2539
|
},
|
|
2182
2540
|
// Test configuration
|
|
2183
|
-
tags:
|
|
2184
|
-
expectedStatus:
|
|
2185
|
-
timeout:
|
|
2186
|
-
retries:
|
|
2187
|
-
annotations: this.extractAnnotations(
|
|
2541
|
+
tags: test2.tags,
|
|
2542
|
+
expectedStatus: test2.expectedStatus,
|
|
2543
|
+
timeout: test2.timeout,
|
|
2544
|
+
retries: test2.retries,
|
|
2545
|
+
annotations: this.extractAnnotations(test2.annotations),
|
|
2188
2546
|
// Execution context
|
|
2189
2547
|
retry: result.retry,
|
|
2190
2548
|
workerIndex: result.workerIndex,
|
|
2191
2549
|
parallelIndex: result.parallelIndex,
|
|
2192
|
-
repeatEachIndex:
|
|
2550
|
+
repeatEachIndex: test2.repeatEachIndex,
|
|
2193
2551
|
// Hierarchy information
|
|
2194
|
-
parentSuite: this.extractParentSuite(
|
|
2552
|
+
parentSuite: this.extractParentSuite(test2.parent),
|
|
2195
2553
|
// Timing
|
|
2196
2554
|
startTime: result.startTime.getTime()
|
|
2197
2555
|
};
|
|
@@ -2200,15 +2558,15 @@ var TestdinoReporter = class {
|
|
|
2200
2558
|
/**
|
|
2201
2559
|
* Called when a test step begins
|
|
2202
2560
|
*/
|
|
2203
|
-
async onStepBegin(
|
|
2561
|
+
async onStepBegin(test2, result, step) {
|
|
2204
2562
|
if (!this.initPromise || this.initFailed) return;
|
|
2205
2563
|
const event = {
|
|
2206
2564
|
type: "step:begin",
|
|
2207
2565
|
runId: this.runId,
|
|
2208
2566
|
...this.getEventMetadata(),
|
|
2209
2567
|
// Step Identification
|
|
2210
|
-
testId:
|
|
2211
|
-
stepId: `${
|
|
2568
|
+
testId: test2.id,
|
|
2569
|
+
stepId: `${test2.id}-${step.titlePath().join("-")}`,
|
|
2212
2570
|
title: step.title,
|
|
2213
2571
|
titlePath: step.titlePath(),
|
|
2214
2572
|
// Step Classification
|
|
@@ -2236,7 +2594,7 @@ var TestdinoReporter = class {
|
|
|
2236
2594
|
/**
|
|
2237
2595
|
* Called when a test step ends
|
|
2238
2596
|
*/
|
|
2239
|
-
async onStepEnd(
|
|
2597
|
+
async onStepEnd(test2, result, step) {
|
|
2240
2598
|
if (!this.initPromise || this.initFailed) return;
|
|
2241
2599
|
const status = step.error ? "failed" : "passed";
|
|
2242
2600
|
const event = {
|
|
@@ -2244,8 +2602,8 @@ var TestdinoReporter = class {
|
|
|
2244
2602
|
runId: this.runId,
|
|
2245
2603
|
...this.getEventMetadata(),
|
|
2246
2604
|
// Step Identification
|
|
2247
|
-
testId:
|
|
2248
|
-
stepId: `${
|
|
2605
|
+
testId: test2.id,
|
|
2606
|
+
stepId: `${test2.id}-${step.titlePath().join("-")}`,
|
|
2249
2607
|
title: step.title,
|
|
2250
2608
|
titlePath: step.titlePath(),
|
|
2251
2609
|
// Timing
|
|
@@ -2272,9 +2630,46 @@ var TestdinoReporter = class {
|
|
|
2272
2630
|
* Called after each test.
|
|
2273
2631
|
* Playwright does not await onTestEnd promises—pending work is awaited in onEnd.
|
|
2274
2632
|
*/
|
|
2275
|
-
onTestEnd(
|
|
2633
|
+
onTestEnd(test2, result) {
|
|
2276
2634
|
if (!this.initPromise || this.initFailed) return;
|
|
2277
|
-
|
|
2635
|
+
if (!this.coverageEnabled && !this.warnedCoverageDisconnect) {
|
|
2636
|
+
const hasCoverageAttachment = result.attachments.some((a) => a.name === "testdino-coverage");
|
|
2637
|
+
if (hasCoverageAttachment) {
|
|
2638
|
+
this.log.warn(
|
|
2639
|
+
"Coverage data detected but coverage.enabled is false \u2014 set coverage: { enabled: true } to collect coverage"
|
|
2640
|
+
);
|
|
2641
|
+
this.warnedCoverageDisconnect = true;
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
if (this.coverageEnabled && this.coverageMerger) {
|
|
2645
|
+
this.extractCoverageFromResult(test2, result, this.coverageMerger);
|
|
2646
|
+
}
|
|
2647
|
+
const projectName = this.getProjectName(test2);
|
|
2648
|
+
if (projectName) {
|
|
2649
|
+
this.projectNames.add(projectName);
|
|
2650
|
+
}
|
|
2651
|
+
if (result.retry > 0) {
|
|
2652
|
+
this.testCounts.retried++;
|
|
2653
|
+
}
|
|
2654
|
+
const isFinalAttempt = result.status === "passed" || result.retry === test2.retries;
|
|
2655
|
+
if (isFinalAttempt) {
|
|
2656
|
+
this.totalTests++;
|
|
2657
|
+
const outcome = test2.outcome();
|
|
2658
|
+
if (outcome === "flaky") {
|
|
2659
|
+
this.testCounts.flaky++;
|
|
2660
|
+
} else if (result.status === "passed") {
|
|
2661
|
+
this.testCounts.passed++;
|
|
2662
|
+
} else if (result.status === "failed") {
|
|
2663
|
+
this.testCounts.failed++;
|
|
2664
|
+
} else if (result.status === "skipped") {
|
|
2665
|
+
this.testCounts.skipped++;
|
|
2666
|
+
} else if (result.status === "timedOut") {
|
|
2667
|
+
this.testCounts.timedOut++;
|
|
2668
|
+
} else if (result.status === "interrupted") {
|
|
2669
|
+
this.testCounts.interrupted++;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
const workPromise = this.processTestEnd(test2, result);
|
|
2278
2673
|
this.pendingTestEndPromises.add(workPromise);
|
|
2279
2674
|
workPromise.finally(() => {
|
|
2280
2675
|
this.pendingTestEndPromises.delete(workPromise);
|
|
@@ -2284,18 +2679,18 @@ var TestdinoReporter = class {
|
|
|
2284
2679
|
* Process test end event asynchronously
|
|
2285
2680
|
* Uploads attachments and adds test:end event to buffer
|
|
2286
2681
|
*/
|
|
2287
|
-
async processTestEnd(
|
|
2682
|
+
async processTestEnd(test2, result) {
|
|
2288
2683
|
try {
|
|
2289
|
-
const attachmentsWithUrls = await this.uploadAttachments(result.attachments,
|
|
2684
|
+
const attachmentsWithUrls = await this.uploadAttachments(result.attachments, test2.id);
|
|
2290
2685
|
const event = {
|
|
2291
2686
|
type: "test:end",
|
|
2292
2687
|
runId: this.runId,
|
|
2293
2688
|
...this.getEventMetadata(),
|
|
2294
2689
|
// Test Identification
|
|
2295
|
-
testId:
|
|
2690
|
+
testId: test2.id,
|
|
2296
2691
|
// Status Information
|
|
2297
2692
|
status: result.status,
|
|
2298
|
-
outcome:
|
|
2693
|
+
outcome: test2.outcome(),
|
|
2299
2694
|
// Timing
|
|
2300
2695
|
duration: result.duration,
|
|
2301
2696
|
// Execution Context
|
|
@@ -2346,6 +2741,43 @@ var TestdinoReporter = class {
|
|
|
2346
2741
|
this.log.debug(`Waiting for ${this.pendingTestEndPromises.size} pending test:end events...`);
|
|
2347
2742
|
await Promise.allSettled(Array.from(this.pendingTestEndPromises));
|
|
2348
2743
|
}
|
|
2744
|
+
if (this.coverageEnabled && this.coverageMerger?.hasCoverage) {
|
|
2745
|
+
try {
|
|
2746
|
+
if (this.projectNames.size > 1 && !this.config.coverage?.projects?.length) {
|
|
2747
|
+
this.log.warn(
|
|
2748
|
+
`Coverage collected from ${this.projectNames.size} projects (${Array.from(this.projectNames).join(", ")}). Set coverage.projects to a single project to avoid duplicate counts`
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
if (this.config.coverage?.projects?.length) {
|
|
2752
|
+
const unmatched = this.config.coverage.projects.filter((p) => !this.projectNames.has(p));
|
|
2753
|
+
if (unmatched.length > 0) {
|
|
2754
|
+
this.log.warn(
|
|
2755
|
+
`coverage.projects contains unmatched values: ${unmatched.join(", ")}. Available projects: ${Array.from(this.projectNames).join(", ")}`
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
const coverageEvent = this.buildCoverageEvent(this.coverageMerger);
|
|
2760
|
+
await this.buffer.add(coverageEvent);
|
|
2761
|
+
this.lastCoverageEvent = coverageEvent;
|
|
2762
|
+
const thresholdFailures = this.checkCoverageThresholds(coverageEvent.summary);
|
|
2763
|
+
if (thresholdFailures.length > 0) {
|
|
2764
|
+
this.coverageThresholdFailed = true;
|
|
2765
|
+
}
|
|
2766
|
+
if (this.config.coverage?.localReport) {
|
|
2767
|
+
const outputDir = this.config.coverage.localReportDir || "./testdino-coverage";
|
|
2768
|
+
const reportPath = await generateIstanbulHtmlReport(this.coverageMerger, {
|
|
2769
|
+
outputDir
|
|
2770
|
+
});
|
|
2771
|
+
this.log.info(`Coverage Report: ${reportPath}`);
|
|
2772
|
+
}
|
|
2773
|
+
} catch (error) {
|
|
2774
|
+
this.log.warn(`Failed to build coverage event: ${error instanceof Error ? error.message : String(error)}`);
|
|
2775
|
+
}
|
|
2776
|
+
} else if (this.coverageEnabled && !this.coverageMerger?.hasCoverage) {
|
|
2777
|
+
this.log.warn(
|
|
2778
|
+
"Coverage enabled but no data was collected. Ensure your app is instrumented with Istanbul (babel-plugin-istanbul) and tests use { test } from @testdino/playwright"
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2349
2781
|
const event = {
|
|
2350
2782
|
type: "run:end",
|
|
2351
2783
|
runId: this.runId,
|
|
@@ -2396,6 +2828,28 @@ var TestdinoReporter = class {
|
|
|
2396
2828
|
this.log.error(`Failed to send run:end event: ${errorMessage}`);
|
|
2397
2829
|
}
|
|
2398
2830
|
}
|
|
2831
|
+
const summaryData = {
|
|
2832
|
+
runId: this.runId,
|
|
2833
|
+
runMetadata: this.runMetadata,
|
|
2834
|
+
testCounts: this.testCounts,
|
|
2835
|
+
totalTests: this.totalTests,
|
|
2836
|
+
workerCount: this.workerCount,
|
|
2837
|
+
projectNames: this.projectNames,
|
|
2838
|
+
shardInfo: this.shardInfo,
|
|
2839
|
+
useHttpFallback: this.useHttpFallback,
|
|
2840
|
+
lastCoverageEvent: this.lastCoverageEvent
|
|
2841
|
+
};
|
|
2842
|
+
this.log.printRunSummary(result, delivered, summaryData);
|
|
2843
|
+
if (this.coverageThresholdFailed && this.lastCoverageEvent) {
|
|
2844
|
+
const failures = this.checkCoverageThresholds(this.lastCoverageEvent.summary);
|
|
2845
|
+
this.log.error("Coverage thresholds not met:");
|
|
2846
|
+
for (const msg of failures) {
|
|
2847
|
+
this.log.error(` ${msg}`);
|
|
2848
|
+
}
|
|
2849
|
+
this.wsClient?.close();
|
|
2850
|
+
this.removeSignalHandlers();
|
|
2851
|
+
return { status: "failed" };
|
|
2852
|
+
}
|
|
2399
2853
|
this.wsClient?.close();
|
|
2400
2854
|
this.removeSignalHandlers();
|
|
2401
2855
|
}
|
|
@@ -2416,7 +2870,7 @@ var TestdinoReporter = class {
|
|
|
2416
2870
|
/**
|
|
2417
2871
|
* Called when standard output is produced in worker process
|
|
2418
2872
|
*/
|
|
2419
|
-
async onStdOut(chunk,
|
|
2873
|
+
async onStdOut(chunk, test2, result) {
|
|
2420
2874
|
if (!this.initPromise || this.initFailed) return;
|
|
2421
2875
|
const { text, truncated } = this.truncateChunk(chunk);
|
|
2422
2876
|
const event = {
|
|
@@ -2426,7 +2880,7 @@ var TestdinoReporter = class {
|
|
|
2426
2880
|
// Console Output
|
|
2427
2881
|
text,
|
|
2428
2882
|
// Test Association (optional)
|
|
2429
|
-
testId:
|
|
2883
|
+
testId: test2?.id,
|
|
2430
2884
|
retry: result?.retry,
|
|
2431
2885
|
// Truncation Indicator
|
|
2432
2886
|
truncated
|
|
@@ -2436,7 +2890,7 @@ var TestdinoReporter = class {
|
|
|
2436
2890
|
/**
|
|
2437
2891
|
* Called when standard error is produced in worker process
|
|
2438
2892
|
*/
|
|
2439
|
-
async onStdErr(chunk,
|
|
2893
|
+
async onStdErr(chunk, test2, result) {
|
|
2440
2894
|
if (!this.initPromise || this.initFailed) return;
|
|
2441
2895
|
const { text, truncated } = this.truncateChunk(chunk);
|
|
2442
2896
|
const event = {
|
|
@@ -2446,7 +2900,7 @@ var TestdinoReporter = class {
|
|
|
2446
2900
|
// Console Error Output
|
|
2447
2901
|
text,
|
|
2448
2902
|
// Test Association (optional)
|
|
2449
|
-
testId:
|
|
2903
|
+
testId: test2?.id,
|
|
2450
2904
|
retry: result?.retry,
|
|
2451
2905
|
// Truncation Indicator
|
|
2452
2906
|
truncated
|
|
@@ -3005,8 +3459,104 @@ var TestdinoReporter = class {
|
|
|
3005
3459
|
getBaseServerUrl(serverUrl) {
|
|
3006
3460
|
return serverUrl.replace(/\/api\/reporter$/, "");
|
|
3007
3461
|
}
|
|
3462
|
+
/**
|
|
3463
|
+
* Walk up the suite hierarchy to find the project name for a test.
|
|
3464
|
+
*/
|
|
3465
|
+
getProjectName(test2) {
|
|
3466
|
+
let suite = test2.parent;
|
|
3467
|
+
while (suite) {
|
|
3468
|
+
const project = suite.project();
|
|
3469
|
+
if (project) {
|
|
3470
|
+
return project.name || project.use?.defaultBrowserType || "default";
|
|
3471
|
+
}
|
|
3472
|
+
suite = suite.parent;
|
|
3473
|
+
}
|
|
3474
|
+
return void 0;
|
|
3475
|
+
}
|
|
3476
|
+
/**
|
|
3477
|
+
* Extract coverage fragment from test result attachments and merge incrementally.
|
|
3478
|
+
* The fixture attaches coverage as an in-memory JSON attachment named 'testdino-coverage'.
|
|
3479
|
+
*
|
|
3480
|
+
* Respects coverage.projects filter to avoid duplicate coverage from multiple browser projects.
|
|
3481
|
+
*/
|
|
3482
|
+
extractCoverageFromResult(test2, result, merger) {
|
|
3483
|
+
if (this.config.coverage?.projects) {
|
|
3484
|
+
const projectName = this.getProjectName(test2);
|
|
3485
|
+
if (projectName && !this.config.coverage.projects.includes(projectName)) {
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
const coverageAttachment = result.attachments.find((a) => a.name === "testdino-coverage");
|
|
3490
|
+
if (!coverageAttachment?.body) return;
|
|
3491
|
+
try {
|
|
3492
|
+
const fragment = JSON.parse(coverageAttachment.body.toString());
|
|
3493
|
+
merger.addFragment(fragment);
|
|
3494
|
+
} catch (error) {
|
|
3495
|
+
this.log.debug(
|
|
3496
|
+
`Malformed coverage attachment, skipping: ${error instanceof Error ? error.message : String(error)}`
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
/**
|
|
3501
|
+
* Build a coverage:data event from the merged coverage data.
|
|
3502
|
+
*
|
|
3503
|
+
* Non-sharded runs: summary + per-file metrics only (small payload).
|
|
3504
|
+
* Sharded runs: also includes compact hit counts for server-side cross-shard merging.
|
|
3505
|
+
*/
|
|
3506
|
+
buildCoverageEvent(merger) {
|
|
3507
|
+
const rootDir = this.runMetadata?.playwright?.rootDir || process.cwd();
|
|
3508
|
+
const summary = merger.computeSummary();
|
|
3509
|
+
const files = merger.computeFileCoverage(rootDir);
|
|
3510
|
+
const isSharded = !!this.shardInfo;
|
|
3511
|
+
if (files.length > COVERAGE_FILE_COUNT_WARNING) {
|
|
3512
|
+
this.log.warn(`Coverage includes ${files.length} files \u2014 consider using coverage.projects to reduce scope`);
|
|
3513
|
+
}
|
|
3514
|
+
const event = {
|
|
3515
|
+
type: "coverage:data",
|
|
3516
|
+
runId: this.runId,
|
|
3517
|
+
...this.getEventMetadata(),
|
|
3518
|
+
summary,
|
|
3519
|
+
files,
|
|
3520
|
+
metadata: {
|
|
3521
|
+
instrumentationType: "istanbul",
|
|
3522
|
+
fileCount: files.length,
|
|
3523
|
+
sharded: isSharded
|
|
3524
|
+
}
|
|
3525
|
+
};
|
|
3526
|
+
if (isSharded) {
|
|
3527
|
+
const coverageMapJSON = merger.toJSON();
|
|
3528
|
+
event.compactCounts = extractCompactCounts(coverageMapJSON, rootDir);
|
|
3529
|
+
event.shard = this.shardInfo;
|
|
3530
|
+
}
|
|
3531
|
+
return event;
|
|
3532
|
+
}
|
|
3533
|
+
/**
|
|
3534
|
+
* Check coverage against configured thresholds.
|
|
3535
|
+
* Returns an array of failure messages (empty if all thresholds pass).
|
|
3536
|
+
*/
|
|
3537
|
+
checkCoverageThresholds(summary) {
|
|
3538
|
+
const thresholds = this.config.coverage?.thresholds;
|
|
3539
|
+
if (!thresholds) return [];
|
|
3540
|
+
const failures = [];
|
|
3541
|
+
const check = (name, actual, threshold) => {
|
|
3542
|
+
if (threshold !== void 0 && actual < threshold) {
|
|
3543
|
+
failures.push(`${name}: ${actual.toFixed(2)}% < ${threshold}%`);
|
|
3544
|
+
}
|
|
3545
|
+
};
|
|
3546
|
+
check("Statements", summary.statements.pct, thresholds.statements);
|
|
3547
|
+
check("Branches", summary.branches.pct, thresholds.branches);
|
|
3548
|
+
check("Functions", summary.functions.pct, thresholds.functions);
|
|
3549
|
+
check("Lines", summary.lines.pct, thresholds.lines);
|
|
3550
|
+
return failures;
|
|
3551
|
+
}
|
|
3008
3552
|
};
|
|
3009
3553
|
|
|
3010
|
-
|
|
3554
|
+
Object.defineProperty(exports, "expect", {
|
|
3555
|
+
enumerable: true,
|
|
3556
|
+
get: function () { return test$1.expect; }
|
|
3557
|
+
});
|
|
3558
|
+
exports.coverageFixtures = coverageFixtures;
|
|
3559
|
+
exports.default = TestdinoReporter;
|
|
3560
|
+
exports.test = test;
|
|
3011
3561
|
//# sourceMappingURL=index.js.map
|
|
3012
3562
|
//# sourceMappingURL=index.js.map
|