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