@piwitests/reporter 0.27.0 → 0.29.0

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 CHANGED
@@ -38,6 +38,8 @@ export default wrapConfig(defineConfig({}), {
38
38
 
39
39
  Run `npx playwright test` — results are uploaded automatically.
40
40
 
41
+ > Piwi rebuilds the run from the data it collects, so you don't also need Playwright's built-in `html` reporter — running it alongside Piwi just repeats the end-of-run report generation. Keep it only if you want Playwright's standalone report uploaded too.
42
+
41
43
  **Recommended: enable the capture fixtures.** One small file unlocks the dashboard's richest features:
42
44
 
43
45
  ```typescript
@@ -83,6 +83,7 @@ var DEFAULTS = {
83
83
  streaming: true,
84
84
  streamingBatchSize: 5,
85
85
  streamingBatchDelay: 2e3,
86
+ maxStreamBufferBytes: 100 * 1024 * 1024,
86
87
  failOnFlakyTests: false,
87
88
  username: null,
88
89
  password: null,
@@ -102,6 +103,7 @@ var PIWI_ENV_KEYS = {
102
103
  streaming: "PIWI_STREAMING",
103
104
  streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
104
105
  streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
106
+ maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
105
107
  liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
106
108
  failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
107
109
  uploadTraces: "PIWI_UPLOAD_TRACES",
@@ -140,6 +142,7 @@ var ENV_FALLBACK_SPECS = [
140
142
  { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
141
143
  { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
142
144
  { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
145
+ { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
143
146
  { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
144
147
  { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
145
148
  { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
@@ -389,6 +392,12 @@ var HttpClient = class {
389
392
  res.on("end", () => {
390
393
  resolve2({ status: res.statusCode ?? 0, text: data, headers: res.headers });
391
394
  });
395
+ res.on("error", reject);
396
+ res.on("close", () => {
397
+ if (!res.complete) {
398
+ reject(new Error(`Connection to ${pathname} closed before the response completed`));
399
+ }
400
+ });
392
401
  }
393
402
  );
394
403
  req.on("error", reject);
@@ -483,6 +492,13 @@ function isUiMode(argv = process.argv) {
483
492
  const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
484
493
  return rest.some((tok) => PW_UI_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
485
494
  }
495
+ var PW_LIST_FLAGS = ["--list"];
496
+ function isListMode(argv = process.argv) {
497
+ const args = argv.slice(2);
498
+ const testIdx = args.indexOf("test");
499
+ const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
500
+ return rest.some((tok) => PW_LIST_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
501
+ }
486
502
 
487
503
  // src/public/global-setup.ts
488
504
  function createGlobalSetup(options, userSetup) {
@@ -512,6 +528,11 @@ function createGlobalSetup(options, userSetup) {
512
528
  if (userSetup) return userSetup(config);
513
529
  return;
514
530
  }
531
+ if (isListMode()) {
532
+ logger.debug("List mode detected \u2014 skipping run registration.");
533
+ if (userSetup) return userSetup(config);
534
+ return;
535
+ }
515
536
  if (opts.enabled === false || !opts.serverUrl) {
516
537
  logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
517
538
  if (userSetup) return userSetup(config);
package/dist/index.d.ts CHANGED
@@ -115,6 +115,18 @@ interface PiwiDashboardOptions {
115
115
  streamingBatchSize?: number;
116
116
  /** Max delay (ms) before flushing pending events during streaming. Defaults to `2000`. */
117
117
  streamingBatchDelay?: number;
118
+ /**
119
+ * Byte budget for the in-memory stream buffer (the queue of events waiting to
120
+ * reach the dashboard, and the crash-recovery file it writes if delivery
121
+ * fails). When the server is unreachable or a huge suite outruns delivery, the
122
+ * queue is capped here instead of growing without bound: the lowest-value
123
+ * events are shed first — live step progress, then per-test `begin` markers,
124
+ * and only as a last resort test results (the final counts and the end-of-run
125
+ * batch submit stay complete regardless). Defaults to `104857600` (100 MB).
126
+ * Set to `0` to disable the cap (unbounded, the pre-`0.28` behavior). Can also
127
+ * be set with `PIWI_MAX_STREAM_BUFFER_BYTES`.
128
+ */
129
+ maxStreamBufferBytes?: number;
118
130
  /**
119
131
  * Fail the run when any test was flaky (passed only after a retry). Forwarded
120
132
  * to Playwright's native `failOnFlakyTests` config option (Playwright 1.52+)
@@ -237,8 +249,10 @@ declare function createGlobalSetup(options?: PiwiDashboardOptions, userSetup?: (
237
249
  *
238
250
  * The `globalSetup` field is set to a `string` (or `string[]` if the user
239
251
  * already has a global setup) referencing the Piwi global setup module,
240
- * which registers the run on the server. The original setup path(s) are
241
- * preserved and executed first.
252
+ * which registers the run on the server. Piwi's module is chained first so the
253
+ * run registers before the user's own setup, appearing as "initializing" on the
254
+ * dashboard while that setup runs; the original setup path(s) are preserved and
255
+ * executed after it.
242
256
  *
243
257
  * Playwright options required in `globalSetup` are forwarded via `PIWI_*`
244
258
  * environment variables (see `applyOptionsToEnv` in `config.ts` for the
@@ -288,6 +302,12 @@ declare class PiwiDashboardReporter {
288
302
  private shardInfo;
289
303
  private metadata;
290
304
  private enabled;
305
+ /**
306
+ * True under `playwright test --list`, where Playwright still constructs this
307
+ * reporter and fires `onBegin`/`onEnd` but runs no tests. Registering and
308
+ * finalizing a run would create an empty phantom run and upload an empty report.
309
+ */
310
+ private readonly listMode;
291
311
  /** True when the server URL and API key came from the desktop app, not from config. */
292
312
  private viaDesktopApp;
293
313
  private isFullRun;
package/dist/index.js CHANGED
@@ -82,6 +82,7 @@ var DEFAULTS = {
82
82
  streaming: true,
83
83
  streamingBatchSize: 5,
84
84
  streamingBatchDelay: 2e3,
85
+ maxStreamBufferBytes: 100 * 1024 * 1024,
85
86
  failOnFlakyTests: false,
86
87
  username: null,
87
88
  password: null,
@@ -101,6 +102,7 @@ var PIWI_ENV_KEYS = {
101
102
  streaming: "PIWI_STREAMING",
102
103
  streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
103
104
  streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
105
+ maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
104
106
  liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
105
107
  failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
106
108
  uploadTraces: "PIWI_UPLOAD_TRACES",
@@ -146,6 +148,7 @@ var ENV_FALLBACK_SPECS = [
146
148
  { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
147
149
  { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
148
150
  { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
151
+ { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
149
152
  { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
150
153
  { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
151
154
  { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
@@ -436,6 +439,12 @@ var HttpClient = class {
436
439
  res.on("end", () => {
437
440
  resolve5({ status: res.statusCode ?? 0, text: data, headers: res.headers });
438
441
  });
442
+ res.on("error", reject);
443
+ res.on("close", () => {
444
+ if (!res.complete) {
445
+ reject(new Error(`Connection to ${pathname} closed before the response completed`));
446
+ }
447
+ });
439
448
  }
440
449
  );
441
450
  req.on("error", reject);
@@ -1187,6 +1196,20 @@ function resolveScmPrNumber(env) {
1187
1196
  const trimmed = raw?.trim();
1188
1197
  return trimmed && /^\d+$/.test(trimmed) ? trimmed : void 0;
1189
1198
  }
1199
+ function resolveScmBaseBranch(env) {
1200
+ const override = env.PIWI_BASE_BRANCH?.trim();
1201
+ if (override) return override;
1202
+ let target;
1203
+ if (env.JENKINS_URL) target = env.CHANGE_TARGET;
1204
+ else if (env.GITHUB_ACTIONS) target = env.GITHUB_BASE_REF;
1205
+ else if (env.GITLAB_CI) target = env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
1206
+ else if (env.TRAVIS)
1207
+ target = env.TRAVIS_PULL_REQUEST && env.TRAVIS_PULL_REQUEST !== "false" ? env.TRAVIS_BRANCH : void 0;
1208
+ else if (env.TF_BUILD) target = env.SYSTEM_PULLREQUEST_TARGETBRANCH;
1209
+ else if (env.BITBUCKET_BUILD_NUMBER) target = env.BITBUCKET_PR_DESTINATION_BRANCH;
1210
+ const trimmed = target?.trim();
1211
+ return trimmed ? normalizeRef(trimmed) : void 0;
1212
+ }
1190
1213
  function normalizeRef(ref) {
1191
1214
  return ref.replace(/^refs\/heads\//, "");
1192
1215
  }
@@ -1319,6 +1342,8 @@ var MetadataCollector = class {
1319
1342
  if (branch) scm.branch = branch;
1320
1343
  const prNumber = resolveScmPrNumber(process.env);
1321
1344
  if (prNumber) scm.prNumber = prNumber;
1345
+ const baseBranch = resolveScmBaseBranch(process.env);
1346
+ if (baseBranch) scm.baseBranch = baseBranch;
1322
1347
  return Object.keys(scm).length > 0 ? scm : void 0;
1323
1348
  }
1324
1349
  collectCiInfo() {
@@ -1397,6 +1422,132 @@ var MetadataCollector = class {
1397
1422
  // src/internal/streaming/stream-manager.ts
1398
1423
  var fs8 = __toESM(require("fs"));
1399
1424
 
1425
+ // src/internal/streaming/bounded-queue.ts
1426
+ var DROP_TIER = {
1427
+ "step-begin": 0,
1428
+ "step-end": 0,
1429
+ begin: 1,
1430
+ complete: 2
1431
+ };
1432
+ var CRITICAL_TIER = 2;
1433
+ var BoundedEventQueue = class {
1434
+ /**
1435
+ * @param maxBytes Byte budget for the buffered events. `<= 0` or non-finite
1436
+ * disables the bound (unbounded, i.e. the pre-existing behavior).
1437
+ */
1438
+ constructor(maxBytes) {
1439
+ this.maxBytes = maxBytes;
1440
+ this.items = [];
1441
+ this._bytes = 0;
1442
+ this._peakBytes = 0;
1443
+ this._dropped = 0;
1444
+ this._droppedByType = {};
1445
+ this._lostResults = false;
1446
+ }
1447
+ /** Number of buffered events. */
1448
+ get length() {
1449
+ return this.items.length;
1450
+ }
1451
+ /** Whether the queue holds no events. */
1452
+ get isEmpty() {
1453
+ return this.items.length === 0;
1454
+ }
1455
+ /** Approximate byte size of the buffered events. */
1456
+ get bytes() {
1457
+ return this._bytes;
1458
+ }
1459
+ /** Highest byte size the queue reached over its lifetime. */
1460
+ get peakBytes() {
1461
+ return this._peakBytes;
1462
+ }
1463
+ /** How many events have been dropped by eviction. */
1464
+ get droppedCount() {
1465
+ return this._dropped;
1466
+ }
1467
+ /** Dropped-event counts broken down by event type. */
1468
+ get droppedByType() {
1469
+ return this._droppedByType;
1470
+ }
1471
+ /** True once a test-result (`complete`) event had to be shed — real live-data loss. */
1472
+ get lostResults() {
1473
+ return this._lostResults;
1474
+ }
1475
+ /** Append one event to the back of the queue, then evict if over budget. */
1476
+ enqueue(event) {
1477
+ const size = estimateSize(event);
1478
+ this.items.push({ event, size });
1479
+ this._bytes += size;
1480
+ this.afterGrow();
1481
+ }
1482
+ /**
1483
+ * Put events back at the front (oldest position), preserving their order, then
1484
+ * evict. Used to re-queue a failed flush ahead of anything that arrived while
1485
+ * it was in flight, and to replay events reloaded from the on-disk buffer.
1486
+ */
1487
+ prepend(events) {
1488
+ if (events.length === 0) return;
1489
+ const head = [];
1490
+ let added = 0;
1491
+ for (const event of events) {
1492
+ const size = estimateSize(event);
1493
+ head.push({ event, size });
1494
+ added += size;
1495
+ }
1496
+ this.items = head.concat(this.items);
1497
+ this._bytes += added;
1498
+ this.afterGrow();
1499
+ }
1500
+ /** Remove and return every queued event, resetting the size counter. */
1501
+ takeAll() {
1502
+ const out = this.items.map((it) => it.event);
1503
+ this.items = [];
1504
+ this._bytes = 0;
1505
+ return out;
1506
+ }
1507
+ /** Read the queued events without removing them. */
1508
+ snapshot() {
1509
+ return this.items.map((it) => it.event);
1510
+ }
1511
+ /** Drop every queued event without counting it as an eviction. */
1512
+ clear() {
1513
+ this.items = [];
1514
+ this._bytes = 0;
1515
+ }
1516
+ afterGrow() {
1517
+ if (this._bytes > this._peakBytes) this._peakBytes = this._bytes;
1518
+ this.evict();
1519
+ }
1520
+ // Shed events tier by tier, oldest-first within a tier, until the buffer fits
1521
+ // its budget. A single event larger than the whole budget is kept rather than
1522
+ // leaving the queue empty, so the most recent result always survives.
1523
+ evict() {
1524
+ if (this.maxBytes <= 0 || !Number.isFinite(this.maxBytes)) return;
1525
+ for (let tier = 0; tier <= CRITICAL_TIER && this._bytes > this.maxBytes; tier++) {
1526
+ let i = 0;
1527
+ while (i < this.items.length && this._bytes > this.maxBytes) {
1528
+ if (tier === CRITICAL_TIER && this.items.length <= 1) break;
1529
+ const it = this.items[i];
1530
+ if (DROP_TIER[it.event.type] === tier) {
1531
+ this.items.splice(i, 1);
1532
+ this._bytes -= it.size;
1533
+ this._dropped++;
1534
+ this._droppedByType[it.event.type] = (this._droppedByType[it.event.type] ?? 0) + 1;
1535
+ if (tier === CRITICAL_TIER) this._lostResults = true;
1536
+ } else {
1537
+ i++;
1538
+ }
1539
+ }
1540
+ }
1541
+ }
1542
+ };
1543
+ function estimateSize(event) {
1544
+ try {
1545
+ return JSON.stringify(event).length;
1546
+ } catch {
1547
+ return 0;
1548
+ }
1549
+ }
1550
+
1400
1551
  // src/internal/support/limiter.ts
1401
1552
  function createLimiter(maxConcurrent) {
1402
1553
  const limitValue = Math.max(1, Math.floor(maxConcurrent));
@@ -1463,8 +1614,8 @@ var StreamManager = class {
1463
1614
  this.fileHandler = fileHandler;
1464
1615
  this.options = options;
1465
1616
  this.logger = logger;
1466
- this.pendingEvents = [];
1467
- this.pendingBeginEvents = [];
1617
+ /** Guards `reportBufferPressure` so its summary is logged at most once. */
1618
+ this.bufferPressureReported = false;
1468
1619
  this.flushTimer = null;
1469
1620
  this.flushPromises = [];
1470
1621
  this.liveUploadPromises = [];
@@ -1489,6 +1640,9 @@ var StreamManager = class {
1489
1640
  this._token = null;
1490
1641
  this._auth = null;
1491
1642
  this._startPromise = null;
1643
+ const maxBytes = this.options.maxStreamBufferBytes ?? 0;
1644
+ this.pendingEvents = new BoundedEventQueue(maxBytes);
1645
+ this.pendingBeginEvents = new BoundedEventQueue(maxBytes);
1492
1646
  }
1493
1647
  /** Whether the streaming session is active */
1494
1648
  get enabled() {
@@ -1510,6 +1664,15 @@ var StreamManager = class {
1510
1664
  get startPromise() {
1511
1665
  return this._startPromise;
1512
1666
  }
1667
+ /**
1668
+ * Whether buffer pressure forced a test-result (`complete`) event to be
1669
+ * dropped from the live stream. When true the submitter finalizes via the
1670
+ * end-of-run batch (which re-sends the full run from the reporter's own
1671
+ * memory) instead of `/finish`, so the dropped detail is not lost.
1672
+ */
1673
+ get bufferLostResults() {
1674
+ return this.pendingEvents.lostResults || this.pendingBeginEvents.lostResults;
1675
+ }
1513
1676
  /** Begin the streaming session after `onBegin` fires. Non-blocking — the actual handshake runs asynchronously. */
1514
1677
  start(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails) {
1515
1678
  this._startPromise = this._doStart(
@@ -1603,8 +1766,7 @@ var StreamManager = class {
1603
1766
  this.lastActivityAt = Date.now();
1604
1767
  this.scheduleHeartbeat();
1605
1768
  if (this.pendingBeginEvents.length > 0) {
1606
- this.pendingEvents = [...this.pendingBeginEvents, ...this.pendingEvents];
1607
- this.pendingBeginEvents = [];
1769
+ this.pendingEvents.prepend(this.pendingBeginEvents.takeAll());
1608
1770
  }
1609
1771
  if (this.pendingEvents.length > 0) this.flush();
1610
1772
  }
@@ -1628,12 +1790,12 @@ var StreamManager = class {
1628
1790
  if (this._enabled && this._runId) {
1629
1791
  this.queueEvent(event);
1630
1792
  } else {
1631
- this.pendingBeginEvents.push(event);
1793
+ this.pendingBeginEvents.enqueue(event);
1632
1794
  }
1633
1795
  }
1634
1796
  /** Queue a test-case event. Triggers an immediate flush when the batch size is reached, otherwise schedules a timer-based flush. */
1635
1797
  queueEvent(event) {
1636
- this.pendingEvents.push(event);
1798
+ this.pendingEvents.enqueue(event);
1637
1799
  if (this.pendingEvents.length >= this.options.streamingBatchSize) {
1638
1800
  this.flush();
1639
1801
  } else if (!this.flushTimer) {
@@ -1646,8 +1808,8 @@ var StreamManager = class {
1646
1808
  clearTimeout(this.flushTimer);
1647
1809
  this.flushTimer = null;
1648
1810
  }
1649
- if (this.pendingEvents.length === 0 || !this._enabled || !this._runId) return null;
1650
- const events = this.pendingEvents.splice(0);
1811
+ if (this.pendingEvents.isEmpty || !this._enabled || !this._runId) return null;
1812
+ const events = this.pendingEvents.takeAll();
1651
1813
  const promise = this.httpClient.postJSON(`/api/test-runs/${this._runId}/events`, { streamToken: this._token, testCases: events }, this._auth).then(
1652
1814
  () => {
1653
1815
  this.retryCount = 0;
@@ -1655,7 +1817,7 @@ var StreamManager = class {
1655
1817
  return true;
1656
1818
  },
1657
1819
  (error) => {
1658
- this.pendingEvents = events.concat(this.pendingEvents);
1820
+ this.pendingEvents.prepend(events);
1659
1821
  this.scheduleRetry(errorMessage(error));
1660
1822
  return false;
1661
1823
  }
@@ -1676,7 +1838,7 @@ var StreamManager = class {
1676
1838
  const buffered = this.streamBuffer.load();
1677
1839
  if (buffered.length > 0) {
1678
1840
  this.streamBuffer.clear();
1679
- this.pendingEvents = buffered.concat(this.pendingEvents);
1841
+ this.pendingEvents.prepend(buffered);
1680
1842
  }
1681
1843
  if (this.pendingEvents.length > 0) this.flush();
1682
1844
  }, delay);
@@ -1728,26 +1890,33 @@ var StreamManager = class {
1728
1890
  }
1729
1891
  /** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
1730
1892
  async drain() {
1893
+ try {
1894
+ await this._drain();
1895
+ } finally {
1896
+ this.reportBufferPressure();
1897
+ }
1898
+ }
1899
+ async _drain() {
1731
1900
  this.stopHeartbeat();
1732
1901
  this.clearRetryTimer();
1733
1902
  if (!this._enabled) {
1734
- this.pendingEvents = [];
1903
+ this.pendingEvents.clear();
1735
1904
  this.flushPromises = [];
1736
1905
  return;
1737
1906
  }
1738
1907
  try {
1739
1908
  const MAX_ATTEMPTS = 10;
1740
1909
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
1741
- if (this._enabled && this.pendingEvents.length > 0) this.flush();
1910
+ if (this._enabled && !this.pendingEvents.isEmpty) this.flush();
1742
1911
  if (this.flushPromises.length > 0) {
1743
1912
  await Promise.allSettled(this.flushPromises);
1744
1913
  this.flushPromises = [];
1745
1914
  }
1746
- if (this.pendingEvents.length === 0) {
1915
+ if (this.pendingEvents.isEmpty) {
1747
1916
  const buffered = this.streamBuffer.load();
1748
1917
  if (buffered.length > 0) {
1749
- this.pendingEvents = buffered;
1750
1918
  this.streamBuffer.clear();
1919
+ this.pendingEvents.prepend(buffered);
1751
1920
  continue;
1752
1921
  }
1753
1922
  return;
@@ -1762,17 +1931,41 @@ var StreamManager = class {
1762
1931
  );
1763
1932
  await new Promise((resolve5) => setTimeout(resolve5, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
1764
1933
  }
1765
- if (this.pendingEvents.length > 0) {
1934
+ if (!this.pendingEvents.isEmpty) {
1935
+ const remaining = this.pendingEvents.takeAll();
1766
1936
  this.logger.warn(
1767
- `Could not deliver ${this.pendingEvents.length} live event(s) to the dashboard \u2014 continuing with the end-of-run submit.`
1937
+ `Could not deliver ${remaining.length} live event(s) to the dashboard \u2014 continuing with the end-of-run submit.`
1768
1938
  );
1769
- this.streamBuffer.append(this.pendingEvents);
1770
- this.pendingEvents = [];
1939
+ this.streamBuffer.append(remaining);
1771
1940
  }
1772
1941
  } finally {
1773
1942
  this.clearRetryTimer();
1774
1943
  }
1775
1944
  }
1945
+ // Emit a single summary when buffer pressure shed events, so a full disk or a
1946
+ // stalled server does not silently swallow data. Reported once per drain.
1947
+ reportBufferPressure() {
1948
+ if (this.bufferPressureReported) return;
1949
+ const dropped = this.pendingEvents.droppedCount + this.pendingBeginEvents.droppedCount;
1950
+ if (dropped === 0) return;
1951
+ this.bufferPressureReported = true;
1952
+ const byType = this.pendingEvents.droppedByType;
1953
+ const beginByType = this.pendingBeginEvents.droppedByType;
1954
+ const count = (type) => (byType[type] ?? 0) + (beginByType[type] ?? 0);
1955
+ const steps = count("step-begin") + count("step-end");
1956
+ const begins = count("begin");
1957
+ const results = count("complete");
1958
+ const limitMb = ((this.options.maxStreamBufferBytes ?? 0) / (1024 * 1024)).toFixed(0);
1959
+ if (results > 0) {
1960
+ this.logger.warn(
1961
+ `Stream buffer hit its ${limitMb} MB limit \u2014 dropped ${dropped} live event(s), including ${results} test result(s) that will not show live on the dashboard (the run's final counts stay accurate, and the end-of-run submit still carries the full run). Raise \`maxStreamBufferBytes\` or check the dashboard connection.`
1962
+ );
1963
+ } else {
1964
+ this.logger.warn(
1965
+ `Stream buffer hit its ${limitMb} MB limit \u2014 dropped ${dropped} live progress event(s) (${steps} step, ${begins} begin) to stay within budget. No test results were lost.`
1966
+ );
1967
+ }
1968
+ }
1776
1969
  /** Schedule a live upload of trace and attachment files for a test case. Skips cases with no files. Concurrency is limited to 2 simultaneous uploads. */
1777
1970
  scheduleLiveUpload(tc) {
1778
1971
  const hasTrace = !!this.options.uploadTraces && this.fileHandler.findTraceFiles(tc).some((p) => fs8.existsSync(p));
@@ -2190,6 +2383,22 @@ function readSelectionStamp(env = process.env) {
2190
2383
  return { key, version, resolvedHash, resolvedCount };
2191
2384
  }
2192
2385
 
2386
+ // src/internal/support/run-mode.ts
2387
+ var PW_UI_FLAGS = ["--ui", "--ui-host", "--ui-port"];
2388
+ function isUiMode(argv = process.argv) {
2389
+ const args = argv.slice(2);
2390
+ const testIdx = args.indexOf("test");
2391
+ const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
2392
+ return rest.some((tok) => PW_UI_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
2393
+ }
2394
+ var PW_LIST_FLAGS = ["--list"];
2395
+ function isListMode(argv = process.argv) {
2396
+ const args = argv.slice(2);
2397
+ const testIdx = args.indexOf("test");
2398
+ const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
2399
+ return rest.some((tok) => PW_LIST_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
2400
+ }
2401
+
2193
2402
  // src/public/global-setup.ts
2194
2403
  var path11 = __toESM(require("path"));
2195
2404
  var fs12 = __toESM(require("fs"));
@@ -2243,15 +2452,6 @@ function isDueForAriaSample(testInfo) {
2243
2452
  return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
2244
2453
  }
2245
2454
 
2246
- // src/internal/support/run-mode.ts
2247
- var PW_UI_FLAGS = ["--ui", "--ui-host", "--ui-port"];
2248
- function isUiMode(argv = process.argv) {
2249
- const args = argv.slice(2);
2250
- const testIdx = args.indexOf("test");
2251
- const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
2252
- return rest.some((tok) => PW_UI_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
2253
- }
2254
-
2255
2455
  // src/public/global-setup.ts
2256
2456
  function createGlobalSetup(options, userSetup) {
2257
2457
  return async function globalSetupFn(config) {
@@ -2280,6 +2480,11 @@ function createGlobalSetup(options, userSetup) {
2280
2480
  if (userSetup) return userSetup(config);
2281
2481
  return;
2282
2482
  }
2483
+ if (isListMode()) {
2484
+ logger.debug("List mode detected \u2014 skipping run registration.");
2485
+ if (userSetup) return userSetup(config);
2486
+ return;
2487
+ }
2283
2488
  if (opts.enabled === false || !opts.serverUrl) {
2284
2489
  logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
2285
2490
  if (userSetup) return userSetup(config);
@@ -2429,12 +2634,11 @@ function resolveSetupModule() {
2429
2634
  }
2430
2635
  function wrapConfig(config, piwiOptions) {
2431
2636
  if (piwiOptions) applyOptionsToEnv(piwiOptions);
2432
- const globalSetupModules = [];
2637
+ const globalSetupModules = [resolveSetupModule()];
2433
2638
  if (config.globalSetup) {
2434
2639
  const orig = Array.isArray(config.globalSetup) ? config.globalSetup : [config.globalSetup];
2435
2640
  globalSetupModules.push(...orig);
2436
2641
  }
2437
- globalSetupModules.push(resolveSetupModule());
2438
2642
  const forwarded = {};
2439
2643
  const failOnFlaky = piwiOptions?.failOnFlakyTests ?? readBool(process.env[PIWI_ENV_KEYS.failOnFlakyTests]);
2440
2644
  if (failOnFlaky === true) forwarded.failOnFlakyTests = true;
@@ -2807,7 +3011,7 @@ var RunSubmitter = class {
2807
3011
  }
2808
3012
  if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
2809
3013
  let outcome = { done: false, output: null };
2810
- if (sm?.enabled && sm?.runId != null) {
3014
+ if (sm?.enabled && sm?.runId != null && !sm.bufferLostResults) {
2811
3015
  outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
2812
3016
  }
2813
3017
  if (!outcome.done && (this.hasReports(run) || run.options.uploadTraces)) {
@@ -3834,6 +4038,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
3834
4038
  this.setupSteps = [];
3835
4039
  this.options = resolveOptions(rawOptions);
3836
4040
  this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
4041
+ this.listMode = isListMode();
3837
4042
  this.viaDesktopApp = usedDesktopDiscovery();
3838
4043
  this.runLabel = this.options.runLabel || detectCiRunLabel();
3839
4044
  this.instanceId = computeInstanceId(this.options.projectName, this.runLabel);
@@ -3875,6 +4080,10 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
3875
4080
  }
3876
4081
  /** Playwright reporter hook: called once at the start of the test run */
3877
4082
  onBegin(config, suite) {
4083
+ if (this.listMode) {
4084
+ this.logger.debug("List mode (--list) detected \u2014 no run registered or report uploaded.");
4085
+ return;
4086
+ }
3878
4087
  if (!this.enabled) {
3879
4088
  this.logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
3880
4089
  return;
@@ -4156,7 +4365,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
4156
4365
  }
4157
4366
  /** Playwright reporter hook: called when the full test run finishes */
4158
4367
  async onEnd(result) {
4159
- if (!this.enabled) return;
4368
+ if (this.listMode || !this.enabled) return;
4160
4369
  const unrunReason = resolveUnrunReason(result?.status, {
4161
4370
  maxFailures: this.maxFailures,
4162
4371
  failures: this.failedTests + this.timedOutTests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piwitests/reporter",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "description": "Playwright reporter that streams results, traces and HTML reports to a Piwi Dashboard instance",
5
5
  "url": "https://github.com/PiwiTests/platform",
6
6
  "homepage": "https://piwitests.dev",