@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/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 path = require('path');
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
- // src/utils/index.ts
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((test) => this.buildSkeletonTest(test))
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(test) {
1559
+ buildSkeletonTest(test2) {
1547
1560
  const skeletonTest = {
1548
- testId: test.id,
1549
- title: test.title,
1561
+ testId: test2.id,
1562
+ title: test2.title,
1550
1563
  location: {
1551
- file: test.location.file,
1552
- line: test.location.line,
1553
- column: test.location.column
1564
+ file: test2.location.file,
1565
+ line: test2.location.line,
1566
+ column: test2.location.column
1554
1567
  }
1555
1568
  };
1556
- if (test.tags && test.tags.length > 0) {
1557
- skeletonTest.tags = test.tags;
1569
+ if (test2.tags && test2.tags.length > 0) {
1570
+ skeletonTest.tags = test2.tags;
1558
1571
  }
1559
- if (test.expectedStatus) {
1560
- skeletonTest.expectedStatus = test.expectedStatus;
1572
+ if (test2.expectedStatus) {
1573
+ skeletonTest.expectedStatus = test2.expectedStatus;
1561
1574
  }
1562
- if (test.annotations && test.annotations.length > 0) {
1563
- skeletonTest.annotations = test.annotations.map((ann) => ({
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
- // src/reporter/log.ts
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(test, result) {
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: test.id,
2174
- title: test.title,
2175
- titlePath: test.titlePath(),
2531
+ testId: test2.id,
2532
+ title: test2.title,
2533
+ titlePath: test2.titlePath(),
2176
2534
  // Location information
2177
2535
  location: {
2178
- file: test.location.file,
2179
- line: test.location.line,
2180
- column: test.location.column
2536
+ file: test2.location.file,
2537
+ line: test2.location.line,
2538
+ column: test2.location.column
2181
2539
  },
2182
2540
  // Test configuration
2183
- tags: test.tags,
2184
- expectedStatus: test.expectedStatus,
2185
- timeout: test.timeout,
2186
- retries: test.retries,
2187
- annotations: this.extractAnnotations(test.annotations),
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: test.repeatEachIndex,
2550
+ repeatEachIndex: test2.repeatEachIndex,
2193
2551
  // Hierarchy information
2194
- parentSuite: this.extractParentSuite(test.parent),
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(test, result, step) {
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: test.id,
2211
- stepId: `${test.id}-${step.titlePath().join("-")}`,
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(test, result, step) {
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: test.id,
2248
- stepId: `${test.id}-${step.titlePath().join("-")}`,
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(test, result) {
2633
+ onTestEnd(test2, result) {
2276
2634
  if (!this.initPromise || this.initFailed) return;
2277
- const workPromise = this.processTestEnd(test, result);
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(test, result) {
2682
+ async processTestEnd(test2, result) {
2288
2683
  try {
2289
- const attachmentsWithUrls = await this.uploadAttachments(result.attachments, test.id);
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: test.id,
2690
+ testId: test2.id,
2296
2691
  // Status Information
2297
2692
  status: result.status,
2298
- outcome: test.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, test, result) {
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: test?.id,
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, test, result) {
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: test?.id,
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
- module.exports = TestdinoReporter;
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