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