@piwitests/reporter 0.42.0 → 0.43.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/dist/cli/index.js CHANGED
@@ -2461,6 +2461,7 @@ var DEFAULTS = {
2461
2461
  streamingBatchSize: 5,
2462
2462
  streamingBatchDelay: 2e3,
2463
2463
  maxStreamBufferBytes: 100 * 1024 * 1024,
2464
+ submitTimeout: 15 * 60 * 1e3,
2464
2465
  failOnFlakyTests: false,
2465
2466
  username: null,
2466
2467
  password: null,
@@ -2482,6 +2483,7 @@ var PIWI_ENV_KEYS = {
2482
2483
  streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
2483
2484
  streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
2484
2485
  maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
2486
+ submitTimeout: "PIWI_SUBMIT_TIMEOUT",
2485
2487
  liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
2486
2488
  failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
2487
2489
  uploadTraces: "PIWI_UPLOAD_TRACES",
@@ -2533,6 +2535,7 @@ var ENV_FALLBACK_SPECS = [
2533
2535
  { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
2534
2536
  { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
2535
2537
  { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
2538
+ { option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
2536
2539
  { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
2537
2540
  { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
2538
2541
  { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
@@ -4693,10 +4696,7 @@ function parseUnifiedDiff(text) {
4693
4696
  }
4694
4697
  return files.filter((f) => f.path);
4695
4698
  }
4696
- var DEFAULT_TRANSLATION_GLOBS = [
4697
- "**/{locales,locale,i18n,lang,translations}/**",
4698
- "**/*.resx"
4699
- ];
4699
+ var DEFAULT_TRANSLATION_GLOBS = ["**/{locales,locale,i18n,lang,translations}/**", "**/*.resx"];
4700
4700
  var ANCHOR_ATTRIBUTES = [
4701
4701
  "id",
4702
4702
  "name",
@@ -90,6 +90,7 @@ var DEFAULTS = {
90
90
  streamingBatchSize: 5,
91
91
  streamingBatchDelay: 2e3,
92
92
  maxStreamBufferBytes: 100 * 1024 * 1024,
93
+ submitTimeout: 15 * 60 * 1e3,
93
94
  failOnFlakyTests: false,
94
95
  username: null,
95
96
  password: null,
@@ -111,6 +112,7 @@ var PIWI_ENV_KEYS = {
111
112
  streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
112
113
  streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
113
114
  maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
115
+ submitTimeout: "PIWI_SUBMIT_TIMEOUT",
114
116
  liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
115
117
  failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
116
118
  uploadTraces: "PIWI_UPLOAD_TRACES",
@@ -156,6 +158,7 @@ var ENV_FALLBACK_SPECS = [
156
158
  { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
157
159
  { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
158
160
  { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
161
+ { option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
159
162
  { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
160
163
  { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
161
164
  { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
@@ -251,13 +254,17 @@ var HttpClient = class {
251
254
  /**
252
255
  * @param serverUrl Base URL of the Piwi Dashboard server (e.g. `http://localhost:3000`).
253
256
  * @param logger Prefixed logger for verbose diagnostics.
254
- * @param timeout Socket inactivity timeout in ms (default 30s). A hung
255
- * server now fails fast instead of stalling the reporter.
257
+ * @param timeout Socket inactivity timeout in ms (default 30s), so a hung
258
+ * server fails fast rather than stalling the reporter.
256
259
  */
257
260
  constructor(serverUrl, logger = new Logger(), timeout = 3e4) {
258
261
  this.serverUrl = serverUrl;
259
262
  this.logger = logger;
260
263
  this.timeout = timeout;
264
+ /** Requests in flight, so `close` can abort them. */
265
+ this.inFlight = /* @__PURE__ */ new Set();
266
+ /** Set by `close`: every later request fails with this reason. */
267
+ this.closedReason = null;
261
268
  this.warnIfInsecureTransport();
262
269
  }
263
270
  /**
@@ -268,7 +275,7 @@ var HttpClient = class {
268
275
  * endpoint exposes the key and payloads on the wire. Loopback (localhost /
269
276
  * 127.0.0.1 / ::1) is exempt: that traffic never leaves the machine. This
270
277
  * warns rather than refuses so internal HTTP deployments (e.g. behind a VPN)
271
- * still work, but the exposure is no longer silent.
278
+ * work, while the exposure is still reported.
272
279
  */
273
280
  warnIfInsecureTransport() {
274
281
  let url;
@@ -289,6 +296,16 @@ var HttpClient = class {
289
296
  get baseUrl() {
290
297
  return this.serverUrl;
291
298
  }
299
+ /** Whether `close` was called. */
300
+ get closed() {
301
+ return this.closedReason !== null;
302
+ }
303
+ /** Abort every request in flight and fail every later one with `reason`. */
304
+ close(reason) {
305
+ this.closedReason = reason;
306
+ for (const req of this.inFlight) req.destroy(new Error(reason));
307
+ this.inFlight.clear();
308
+ }
292
309
  /**
293
310
  * Resolve an auth credential: prefer `apiKey`, fall back to `username`/`password` login,
294
311
  * or return `null` when neither is configured.
@@ -409,6 +426,10 @@ var HttpClient = class {
409
426
  /** Unified request core: transport, headers, auth, response accumulation, timeout. */
410
427
  request(method, pathname, opts) {
411
428
  return new Promise((resolve2, reject) => {
429
+ if (this.closedReason !== null) {
430
+ reject(new Error(this.closedReason));
431
+ return;
432
+ }
412
433
  const url = new import_node_url.URL(pathname, this.serverUrl);
413
434
  const transport = url.protocol === "https:" ? https : http;
414
435
  const headers = { ...opts.headers };
@@ -437,11 +458,14 @@ var HttpClient = class {
437
458
  });
438
459
  }
439
460
  );
461
+ this.inFlight.add(req);
462
+ req.on("close", () => this.inFlight.delete(req));
440
463
  req.on("error", reject);
441
464
  req.setTimeout(this.timeout, () => {
442
465
  req.destroy(new Error(`Request to ${pathname} timed out after ${this.timeout}ms`));
443
466
  });
444
467
  if (opts.form) {
468
+ opts.form.on("error", (error) => req.destroy(error));
445
469
  opts.form.pipe(req);
446
470
  } else if (opts.body !== void 0) {
447
471
  req.write(opts.body);
@@ -473,22 +497,30 @@ function computeInstanceId(projectName, runLabel) {
473
497
  }
474
498
 
475
499
  // src/internal/support/ci.ts
476
- function detectCiRunLabel() {
477
- const env = process.env;
478
- if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID) return env.GITHUB_RUN_ID;
479
- if (env.GITLAB_CI && env.CI_PIPELINE_ID) return env.CI_PIPELINE_ID;
480
- if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID) return env.CIRCLE_WORKFLOW_ID;
481
- if (env.TRAVIS && env.TRAVIS_BUILD_ID) return env.TRAVIS_BUILD_ID;
482
- if (env.TF_BUILD && env.BUILD_BUILDID) return env.BUILD_BUILDID;
483
- if (env.JENKINS_URL && env.BUILD_ID) return env.BUILD_ID;
484
- if (env.BUILDKITE_BUILD_ID) return env.BUILDKITE_BUILD_ID;
485
- if (env.TEAMCITY_BUILD_ID) return env.TEAMCITY_BUILD_ID;
486
- if (env.BITBUCKET_BUILD_NUMBER) return env.BITBUCKET_BUILD_NUMBER;
487
- if (env.SEMAPHORE_WORKFLOW_ID) return env.SEMAPHORE_WORKFLOW_ID;
488
- if (env.APPVEYOR_BUILD_ID) return env.APPVEYOR_BUILD_ID;
489
- if (env.DRONE_BUILD_NUMBER) return env.DRONE_BUILD_NUMBER;
500
+ function detectCiIds(env) {
501
+ if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID) {
502
+ const run = env.GITHUB_RUN_ATTEMPT ? `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` : env.GITHUB_RUN_ID;
503
+ return { run, job: env.GITHUB_JOB || null };
504
+ }
505
+ if (env.GITLAB_CI && env.CI_PIPELINE_ID) return { run: env.CI_PIPELINE_ID, job: env.CI_JOB_ID || null };
506
+ if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID) return { run: env.CIRCLE_WORKFLOW_ID, job: env.CIRCLE_BUILD_NUM || null };
507
+ if (env.TRAVIS && env.TRAVIS_BUILD_ID) return { run: env.TRAVIS_BUILD_ID, job: env.TRAVIS_JOB_ID || null };
508
+ if (env.TF_BUILD && env.BUILD_BUILDID) return { run: env.BUILD_BUILDID, job: env.SYSTEM_JOBID || null };
509
+ if (env.JENKINS_URL && env.BUILD_ID) return { run: env.BUILD_ID, job: null };
510
+ if (env.BUILDKITE_BUILD_ID) return { run: env.BUILDKITE_BUILD_ID, job: env.BUILDKITE_JOB_ID || null };
511
+ if (env.TEAMCITY_BUILD_ID) return { run: env.TEAMCITY_BUILD_ID, job: null };
512
+ if (env.BITBUCKET_BUILD_NUMBER) return { run: env.BITBUCKET_BUILD_NUMBER, job: env.BITBUCKET_STEP_UUID || null };
513
+ if (env.SEMAPHORE_WORKFLOW_ID) return { run: env.SEMAPHORE_WORKFLOW_ID, job: env.SEMAPHORE_JOB_ID || null };
514
+ if (env.APPVEYOR_BUILD_ID) return { run: env.APPVEYOR_BUILD_ID, job: env.APPVEYOR_JOB_ID || null };
515
+ if (env.DRONE_BUILD_NUMBER) return { run: env.DRONE_BUILD_NUMBER, job: null };
490
516
  return null;
491
517
  }
518
+ function resolveRunLabel(configured, sharded) {
519
+ if (configured) return configured;
520
+ const ci = detectCiIds(process.env);
521
+ if (!ci) return null;
522
+ return !sharded && ci.job ? `${ci.run}|${ci.job}` : ci.run;
523
+ }
492
524
 
493
525
  // src/internal/support/shard-info.ts
494
526
  function parseShardSpec(raw) {
@@ -702,7 +734,6 @@ function createGlobalSetup(options, userSetup) {
702
734
  const httpClient = new HttpClient(opts.serverUrl, logger);
703
735
  try {
704
736
  const auth = await httpClient.resolveAuth(opts);
705
- const runLabel = opts.runLabel || detectCiRunLabel();
706
737
  const shard = resolveShardInfo(config);
707
738
  const response = await httpClient.postJSON(
708
739
  "/api/test-runs/setup",
@@ -713,7 +744,7 @@ function createGlobalSetup(options, userSetup) {
713
744
  label: opts.label || null,
714
745
  keep: opts.keep === true,
715
746
  startTime: (/* @__PURE__ */ new Date()).toISOString(),
716
- instanceId: computeInstanceId(opts.projectName, runLabel),
747
+ instanceId: computeInstanceId(opts.projectName, resolveRunLabel(opts.runLabel, shard !== null)),
717
748
  shardIndex: shard?.current,
718
749
  shardTotal: shard?.total
719
750
  },
package/dist/index.d.ts CHANGED
@@ -22,6 +22,15 @@ interface PiwiDashboardOptions {
22
22
  projectName?: string;
23
23
  /** Optional description of the project */
24
24
  projectDescription?: string;
25
+ /**
26
+ * Time budget (ms) for the reporter's end-of-run work: delivering the last
27
+ * live events, uploading the remaining traces and attachments, and submitting
28
+ * the run. When it runs out, the reporter stops waiting for the dashboard and
29
+ * saves the run's results locally; the next run for the project uploads them,
30
+ * without traces or attachments. Defaults to `900000` (15 minutes). Set to `0`
31
+ * for no limit. Can also be set with `PIWI_SUBMIT_TIMEOUT`.
32
+ */
33
+ submitTimeout?: number;
25
34
  /** Upload trace files to the dashboard. Defaults to `true`. */
26
35
  uploadTraces?: boolean;
27
36
  /** Upload the Playwright HTML report. Defaults to `true`. */
@@ -189,7 +198,12 @@ interface PiwiDashboardOptions {
189
198
  dir?: string;
190
199
  label?: string;
191
200
  }>;
192
- /** Stable label that ties shards together (e.g. CI run ID). Auto-detected from CI env; override if needed. */
201
+ /**
202
+ * Stable label that ties shards together (e.g. CI run ID). Auto-detected from
203
+ * CI env, with the CI job's id added for a run that is not sharded; a label
204
+ * set here is used as it is. Override it when your CI is not detected, or to
205
+ * keep matrix legs that report to one project apart.
206
+ */
193
207
  runLabel?: string;
194
208
  /** Deployment environment for this run, e.g. `"production"`, `"staging"`, `"integration"` */
195
209
  environment?: string;
@@ -303,9 +317,8 @@ declare function createGlobalSetup(options?: PiwiDashboardOptions, userSetup?: (
303
317
  * executed after it.
304
318
  *
305
319
  * Playwright options required in `globalSetup` are forwarded via `PIWI_*`
306
- * environment variables (see `applyOptionsToEnv` in `config.ts` for the
307
- * supported set — `serverUrl`, `projectName`, `verbose`, `apiKey`,
308
- * `username`, `password`, `environment`, `label`, `runLabel`).
320
+ * environment variables (see `applyOptionsToEnv` in `internal/config/env.ts`
321
+ * for the supported set).
309
322
  *
310
323
  * The top-level `use` block's `screenshot` and `trace` are defaulted to
311
324
  * `'only-on-failure'` / `'retain-on-failure'` when unset so failure evidence is
@@ -345,8 +358,8 @@ declare class PiwiDashboardReporter {
345
358
  private reportedTestIds;
346
359
  /** Per-test attempt history, keyed by `test.id`; snapshotted onto every attempt's payload. */
347
360
  private attemptsByTest;
361
+ /** Derived in `onBegin`, once the shard is known. */
348
362
  private instanceId;
349
- private runLabel;
350
363
  private shardInfo;
351
364
  private metadata;
352
365
  private enabled;
package/dist/index.js CHANGED
@@ -89,6 +89,7 @@ var DEFAULTS = {
89
89
  streamingBatchSize: 5,
90
90
  streamingBatchDelay: 2e3,
91
91
  maxStreamBufferBytes: 100 * 1024 * 1024,
92
+ submitTimeout: 15 * 60 * 1e3,
92
93
  failOnFlakyTests: false,
93
94
  username: null,
94
95
  password: null,
@@ -110,6 +111,7 @@ var PIWI_ENV_KEYS = {
110
111
  streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
111
112
  streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
112
113
  maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
114
+ submitTimeout: "PIWI_SUBMIT_TIMEOUT",
113
115
  liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
114
116
  failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
115
117
  uploadTraces: "PIWI_UPLOAD_TRACES",
@@ -172,6 +174,7 @@ var ENV_FALLBACK_SPECS = [
172
174
  { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
173
175
  { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
174
176
  { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
177
+ { option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
175
178
  { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
176
179
  { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
177
180
  { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
@@ -316,13 +319,17 @@ var HttpClient = class {
316
319
  /**
317
320
  * @param serverUrl Base URL of the Piwi Dashboard server (e.g. `http://localhost:3000`).
318
321
  * @param logger Prefixed logger for verbose diagnostics.
319
- * @param timeout Socket inactivity timeout in ms (default 30s). A hung
320
- * server now fails fast instead of stalling the reporter.
322
+ * @param timeout Socket inactivity timeout in ms (default 30s), so a hung
323
+ * server fails fast rather than stalling the reporter.
321
324
  */
322
325
  constructor(serverUrl, logger = new Logger(), timeout = 3e4) {
323
326
  this.serverUrl = serverUrl;
324
327
  this.logger = logger;
325
328
  this.timeout = timeout;
329
+ /** Requests in flight, so `close` can abort them. */
330
+ this.inFlight = /* @__PURE__ */ new Set();
331
+ /** Set by `close`: every later request fails with this reason. */
332
+ this.closedReason = null;
326
333
  this.warnIfInsecureTransport();
327
334
  }
328
335
  /**
@@ -333,7 +340,7 @@ var HttpClient = class {
333
340
  * endpoint exposes the key and payloads on the wire. Loopback (localhost /
334
341
  * 127.0.0.1 / ::1) is exempt: that traffic never leaves the machine. This
335
342
  * warns rather than refuses so internal HTTP deployments (e.g. behind a VPN)
336
- * still work, but the exposure is no longer silent.
343
+ * work, while the exposure is still reported.
337
344
  */
338
345
  warnIfInsecureTransport() {
339
346
  let url;
@@ -354,6 +361,16 @@ var HttpClient = class {
354
361
  get baseUrl() {
355
362
  return this.serverUrl;
356
363
  }
364
+ /** Whether `close` was called. */
365
+ get closed() {
366
+ return this.closedReason !== null;
367
+ }
368
+ /** Abort every request in flight and fail every later one with `reason`. */
369
+ close(reason) {
370
+ this.closedReason = reason;
371
+ for (const req of this.inFlight) req.destroy(new Error(reason));
372
+ this.inFlight.clear();
373
+ }
357
374
  /**
358
375
  * Resolve an auth credential: prefer `apiKey`, fall back to `username`/`password` login,
359
376
  * or return `null` when neither is configured.
@@ -474,6 +491,10 @@ var HttpClient = class {
474
491
  /** Unified request core: transport, headers, auth, response accumulation, timeout. */
475
492
  request(method, pathname, opts) {
476
493
  return new Promise((resolve6, reject) => {
494
+ if (this.closedReason !== null) {
495
+ reject(new Error(this.closedReason));
496
+ return;
497
+ }
477
498
  const url = new import_node_url.URL(pathname, this.serverUrl);
478
499
  const transport = url.protocol === "https:" ? https : http;
479
500
  const headers = { ...opts.headers };
@@ -502,11 +523,14 @@ var HttpClient = class {
502
523
  });
503
524
  }
504
525
  );
526
+ this.inFlight.add(req);
527
+ req.on("close", () => this.inFlight.delete(req));
505
528
  req.on("error", reject);
506
529
  req.setTimeout(this.timeout, () => {
507
530
  req.destroy(new Error(`Request to ${pathname} timed out after ${this.timeout}ms`));
508
531
  });
509
532
  if (opts.form) {
533
+ opts.form.on("error", (error) => req.destroy(error));
510
534
  opts.form.pipe(req);
511
535
  } else if (opts.body !== void 0) {
512
536
  req.write(opts.body);
@@ -865,19 +889,25 @@ function computeInstanceId(projectName, runLabel) {
865
889
  // src/internal/streaming/stream-buffer.ts
866
890
  var StreamBuffer = class {
867
891
  constructor(projectName) {
868
- this.filePath = path3.join(os3.tmpdir(), `piwi-dashboard-stream-${hashForProject(projectName)}.jsonl`);
892
+ this.filePath = null;
893
+ this.prefix = `piwi-dashboard-stream-${hashForProject(projectName)}`;
894
+ }
895
+ /** Scope the buffer to a run. Until then the buffer is empty and appends are dropped. */
896
+ bindRun(runId) {
897
+ this.filePath = path3.join(os3.tmpdir(), `${this.prefix}-${runId}.jsonl`);
869
898
  }
870
899
  /** Append one or more events to the on-disk buffer */
871
900
  append(events) {
872
- if (events.length === 0) return;
901
+ if (!this.filePath || events.length === 0) return;
873
902
  try {
874
903
  const lines = events.map((e) => JSON.stringify(e) + "\n").join("");
875
904
  fs3.appendFileSync(this.filePath, lines, "utf8");
876
905
  } catch {
877
906
  }
878
907
  }
879
- /** Load all buffered events from disk, clearing the file */
908
+ /** Load all buffered events of the bound run from disk */
880
909
  load() {
910
+ if (!this.filePath) return [];
881
911
  try {
882
912
  if (fs3.existsSync(this.filePath)) {
883
913
  const content = fs3.readFileSync(this.filePath, "utf8");
@@ -887,23 +917,29 @@ var StreamBuffer = class {
887
917
  }
888
918
  return [];
889
919
  }
890
- /** Delete the buffer file from disk */
920
+ /** Delete the bound run's buffer file from disk */
891
921
  clear() {
922
+ if (!this.filePath) return;
892
923
  try {
893
924
  if (fs3.existsSync(this.filePath)) fs3.unlinkSync(this.filePath);
894
925
  } catch {
895
926
  }
896
927
  }
897
- /** Remove the buffer file if it is older than `maxAgeMs` (default 2 hours). Used on startup to discard orphaned data. */
928
+ /** Remove this project's buffer files older than `maxAgeMs` (default 2 hours). Used on startup to discard orphaned data. */
898
929
  clearStale(maxAgeMs = 72e5) {
930
+ const dir = os3.tmpdir();
931
+ let names;
899
932
  try {
900
- if (fs3.existsSync(this.filePath)) {
901
- const stats = fs3.statSync(this.filePath);
902
- if (Date.now() - stats.mtimeMs > maxAgeMs) {
903
- fs3.unlinkSync(this.filePath);
904
- }
905
- }
933
+ names = fs3.readdirSync(dir).filter((name) => name.startsWith(this.prefix));
906
934
  } catch {
935
+ return;
936
+ }
937
+ for (const name of names) {
938
+ try {
939
+ const file = path3.join(dir, name);
940
+ if (Date.now() - fs3.statSync(file).mtimeMs > maxAgeMs) fs3.unlinkSync(file);
941
+ } catch {
942
+ }
907
943
  }
908
944
  }
909
945
  };
@@ -1332,7 +1368,7 @@ var MetadataCollector = class {
1332
1368
  if (options.tags && Array.isArray(options.tags)) metadata.tags = options.tags;
1333
1369
  if (options.customData) metadata.customData = options.customData;
1334
1370
  if (options.collectScmInfo) {
1335
- const scm = this.collectScmInfo(options);
1371
+ const scm = this.collectScmInfo();
1336
1372
  if (scm) metadata.scm = scm;
1337
1373
  }
1338
1374
  if (options.collectCiInfo) {
@@ -1426,7 +1462,7 @@ var MetadataCollector = class {
1426
1462
  }
1427
1463
  return { suitePath, suiteConfig };
1428
1464
  }
1429
- collectScmInfo(_options) {
1465
+ collectScmInfo() {
1430
1466
  const scm = {};
1431
1467
  let gitBranch;
1432
1468
  try {
@@ -1547,7 +1583,7 @@ var CRITICAL_TIER = 2;
1547
1583
  var BoundedEventQueue = class {
1548
1584
  /**
1549
1585
  * @param maxBytes Byte budget for the buffered events. `<= 0` or non-finite
1550
- * disables the bound (unbounded, i.e. the pre-existing behavior).
1586
+ * disables the bound (unbounded).
1551
1587
  */
1552
1588
  constructor(maxBytes) {
1553
1589
  this.maxBytes = maxBytes;
@@ -1710,10 +1746,14 @@ function runUrl(serverUrl, runId) {
1710
1746
  }
1711
1747
 
1712
1748
  // src/internal/streaming/stream-manager.ts
1749
+ var SERVER_MAX_EVENT_BATCH_BYTES = 10 * 1024 * 1024;
1750
+ var EVENT_ENVELOPE_BYTES = 1024;
1751
+ var MAX_EVENT_REQUEST_BYTES = 4 * 1024 * 1024;
1752
+ var MAX_UPLOAD_CONNECTION_FAILURES = 3;
1713
1753
  var StreamManager = class {
1714
1754
  /**
1715
1755
  * @param httpClient HTTP client for server communication.
1716
- * @param streamBuffer On-disk buffer for crash-safe event persistence.
1756
+ * @param streamBuffer On-disk buffer for the run's undelivered events.
1717
1757
  * @param recovery Crash-recovery handler for uploading stale payloads on startup.
1718
1758
  * @param uploader Uploader for per-test-case file uploads.
1719
1759
  * @param fileHandler File-discovery helper for finding traces and attachments.
@@ -1749,6 +1789,8 @@ var StreamManager = class {
1749
1789
  this.heartbeatInterval = 15e3;
1750
1790
  /** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
1751
1791
  this.uploadedCaseFiles = /* @__PURE__ */ new WeakSet();
1792
+ /** Set when a test-result event left the live stream undelivered: too large to send, or still queued when the drain gave up. */
1793
+ this.resultsUndelivered = false;
1752
1794
  this._enabled = false;
1753
1795
  this._runId = null;
1754
1796
  this._token = null;
@@ -1779,13 +1821,15 @@ var StreamManager = class {
1779
1821
  return this._startPromise;
1780
1822
  }
1781
1823
  /**
1782
- * Whether buffer pressure forced a test-result (`complete`) event to be
1783
- * dropped from the live stream. When true the submitter finalizes via the
1784
- * end-of-run batch (which re-sends the full run from the reporter's own
1785
- * memory) instead of `/finish`, so the dropped detail is not lost.
1824
+ * Whether a test-result (`complete`) event never reached the server through
1825
+ * the live stream: shed under buffer pressure, too large for any request the
1826
+ * server accepts, or still undelivered when the drain gave up. When true the
1827
+ * submitter finalizes via the end-of-run batch (which re-sends the full run
1828
+ * from the reporter's own memory) instead of `/finish`, so the missing
1829
+ * results are not lost.
1786
1830
  */
1787
- get bufferLostResults() {
1788
- return this.pendingEvents.lostResults || this.pendingBeginEvents.lostResults;
1831
+ get lostResults() {
1832
+ return this.resultsUndelivered || this.pendingEvents.lostResults || this.pendingBeginEvents.lostResults;
1789
1833
  }
1790
1834
  /** Begin the streaming session after `onBegin` fires. Non-blocking — the actual handshake runs asynchronously. */
1791
1835
  start(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails, plannedTotal) {
@@ -1878,6 +1922,7 @@ var StreamManager = class {
1878
1922
  this._runId = response.runId;
1879
1923
  this._token = response.streamToken;
1880
1924
  this._enabled = true;
1925
+ this.streamBuffer.bindRun(response.runId);
1881
1926
  this.logger.info(`Streaming enabled. Run ID: ${response.runId}`);
1882
1927
  if (this.options.serverUrl) {
1883
1928
  this.logger.info(`Watch live: ${runUrl(this.options.serverUrl, response.runId)}`);
@@ -1928,21 +1973,78 @@ var StreamManager = class {
1928
1973
  this.flushTimer = null;
1929
1974
  }
1930
1975
  if (this.pendingEvents.isEmpty || !this._enabled || !this._runId) return null;
1931
- const events = this.pendingEvents.takeAll();
1932
- const promise = this.httpClient.postJSON(`/api/test-runs/${this._runId}/events`, { streamToken: this._token, testCases: events }, this._auth).then(
1933
- () => {
1976
+ const promise = this.sendEvents(this.pendingEvents.takeAll());
1977
+ this.flushPromises.push(promise);
1978
+ return promise;
1979
+ }
1980
+ // Post the events in order, in requests of at most `MAX_EVENT_REQUEST_BYTES`.
1981
+ // A request the server refuses as too large (413) is split in half and resent;
1982
+ // a single event it still refuses leaves the live stream. On any other failure
1983
+ // the unsent events go back to the head of the queue and a retry is scheduled.
1984
+ async sendEvents(events) {
1985
+ const chunks = this.chunkEvents(events);
1986
+ while (chunks.length > 0) {
1987
+ const chunk = chunks.shift();
1988
+ try {
1989
+ await this.httpClient.postJSON(
1990
+ `/api/test-runs/${this._runId}/events`,
1991
+ { streamToken: this._token, testCases: chunk },
1992
+ this._auth
1993
+ );
1934
1994
  this.retryCount = 0;
1935
1995
  this.lastActivityAt = Date.now();
1936
- return true;
1937
- },
1938
- (error) => {
1939
- this.pendingEvents.prepend(events);
1996
+ } catch (error) {
1997
+ if (error instanceof HttpError && error.status === 413) {
1998
+ if (chunk.length > 1) {
1999
+ const half = Math.ceil(chunk.length / 2);
2000
+ chunks.unshift(chunk.slice(0, half), chunk.slice(half));
2001
+ } else {
2002
+ this.dropOversized(chunk[0]);
2003
+ }
2004
+ continue;
2005
+ }
2006
+ this.pendingEvents.prepend(chunk.concat(...chunks));
1940
2007
  this.scheduleRetry(errorMessage(error));
1941
2008
  return false;
1942
2009
  }
1943
- );
1944
- this.flushPromises.push(promise);
1945
- return promise;
2010
+ }
2011
+ return true;
2012
+ }
2013
+ // Group events into request-sized chunks, keeping their order. An event
2014
+ // larger than any request the server accepts is dropped here.
2015
+ chunkEvents(events) {
2016
+ const chunks = [];
2017
+ let chunk = [];
2018
+ let chunkBytes = 0;
2019
+ for (const event of events) {
2020
+ const bytes = serializedBytes(event);
2021
+ if (bytes > SERVER_MAX_EVENT_BATCH_BYTES - EVENT_ENVELOPE_BYTES) {
2022
+ this.dropOversized(event);
2023
+ continue;
2024
+ }
2025
+ if (chunk.length > 0 && chunkBytes + bytes > MAX_EVENT_REQUEST_BYTES) {
2026
+ chunks.push(chunk);
2027
+ chunk = [];
2028
+ chunkBytes = 0;
2029
+ }
2030
+ chunk.push(event);
2031
+ chunkBytes += bytes;
2032
+ }
2033
+ if (chunk.length > 0) chunks.push(chunk);
2034
+ return chunks;
2035
+ }
2036
+ // Leave an event the server will not accept out of the live stream. A test
2037
+ // result dropped this way sends the run through the end-of-run batch upload,
2038
+ // which carries the full run (see `lostResults`).
2039
+ dropOversized(event) {
2040
+ if (event.type === "complete") {
2041
+ this.resultsUndelivered = true;
2042
+ this.logger.warn(
2043
+ `The live result of "${event.title}" is too large for the dashboard's event stream \u2014 it is sent with the end-of-run upload instead.`
2044
+ );
2045
+ } else {
2046
+ this.logger.debug(`Dropped a ${event.type} event for "${event.title}" that is too large to stream.`);
2047
+ }
1946
2048
  }
1947
2049
  scheduleRetry(reason) {
1948
2050
  if (this.retryTimer) return;
@@ -2007,7 +2109,7 @@ var StreamManager = class {
2007
2109
  this.retryTimer = null;
2008
2110
  }
2009
2111
  }
2010
- /** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
2112
+ /** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off, until the HTTP client is closed. */
2011
2113
  async drain() {
2012
2114
  try {
2013
2115
  await this._drain();
@@ -2040,6 +2142,7 @@ var StreamManager = class {
2040
2142
  }
2041
2143
  return;
2042
2144
  }
2145
+ if (this.httpClient.closed) break;
2043
2146
  if (attempt === 0) {
2044
2147
  this.logger.warn(
2045
2148
  `The dashboard has not accepted ${this.pendingEvents.length} live event(s) yet \u2014 retrying delivery before the final submit (this can take a few minutes)...`
@@ -2052,6 +2155,7 @@ var StreamManager = class {
2052
2155
  }
2053
2156
  if (!this.pendingEvents.isEmpty) {
2054
2157
  const remaining = this.pendingEvents.takeAll();
2158
+ if (remaining.some((event) => event.type === "complete")) this.resultsUndelivered = true;
2055
2159
  this.logger.warn(
2056
2160
  `Could not deliver ${remaining.length} live event(s) to the dashboard \u2014 continuing with the end-of-run submit.`
2057
2161
  );
@@ -2061,6 +2165,10 @@ var StreamManager = class {
2061
2165
  this.clearRetryTimer();
2062
2166
  }
2063
2167
  }
2168
+ /** Delete the run's on-disk event buffer. Called once the run's results reached the server. */
2169
+ discardBuffered() {
2170
+ this.streamBuffer.clear();
2171
+ }
2064
2172
  // Emit a single summary when buffer pressure shed events, so a full disk or a
2065
2173
  // stalled server does not silently swallow data. Reported once per drain.
2066
2174
  reportBufferPressure() {
@@ -2125,13 +2233,18 @@ var StreamManager = class {
2125
2233
  })();
2126
2234
  this.liveUploadPromises.push(promise);
2127
2235
  }
2128
- /** Wait for all live uploads to settle, then upload files for any test cases that weren't uploaded live */
2236
+ /**
2237
+ * Wait for all live uploads to settle, then upload files for any test cases
2238
+ * that weren't uploaded live. Stops after `MAX_UPLOAD_CONNECTION_FAILURES`
2239
+ * uploads in a row got no response at all.
2240
+ */
2129
2241
  async uploadRemaining(testCases) {
2130
2242
  if (this.liveUploadPromises.length > 0) {
2131
2243
  await Promise.allSettled(this.liveUploadPromises);
2132
2244
  this.liveUploadPromises = [];
2133
2245
  }
2134
2246
  if (!this._enabled || !this._runId || !this._token) return;
2247
+ let connectionFailures = 0;
2135
2248
  for (const tc of testCases) {
2136
2249
  if (this.uploadedCaseFiles.has(tc)) continue;
2137
2250
  try {
@@ -2144,12 +2257,25 @@ var StreamManager = class {
2144
2257
  this._auth
2145
2258
  );
2146
2259
  if (uploaded) this.uploadedCaseFiles.add(tc);
2260
+ connectionFailures = 0;
2147
2261
  } catch (error) {
2148
2262
  this.logger.warn(`Failed to upload files for "${tc.title}": ${errorMessage(error)}`);
2263
+ connectionFailures = error instanceof HttpError ? 0 : connectionFailures + 1;
2264
+ if (connectionFailures >= MAX_UPLOAD_CONNECTION_FAILURES) {
2265
+ this.logger.warn(`The dashboard stopped responding \u2014 skipping the file uploads of the remaining tests.`);
2266
+ return;
2267
+ }
2149
2268
  }
2150
2269
  }
2151
2270
  }
2152
2271
  };
2272
+ function serializedBytes(event) {
2273
+ try {
2274
+ return Buffer.byteLength(JSON.stringify(event)) + 1;
2275
+ } catch {
2276
+ return 0;
2277
+ }
2278
+ }
2153
2279
 
2154
2280
  // ../core/src/locator-chain.ts
2155
2281
  var LOCATING_METHODS = /* @__PURE__ */ new Set([
@@ -2601,15 +2727,19 @@ function readChain(region) {
2601
2727
  const link = /^\s*\.\s*(\w+)\(/.exec(region.slice(scanned.end));
2602
2728
  return link && CHAIN_LINK_METHODS.has(link[1]) ? null : scanned;
2603
2729
  }
2604
- function extractTopFrame(text) {
2730
+ function extractStackFrames(text) {
2605
2731
  const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
2732
+ const frames = [];
2606
2733
  let m;
2607
2734
  while ((m = frameRe.exec(text)) !== null) {
2608
2735
  const file = m[1].replace(/\\/g, "/");
2609
2736
  if (file.includes("node_modules") || file.startsWith("node:")) continue;
2610
- return { file, line: Number(m[2]), column: Number(m[3]) };
2737
+ frames.push({ file, line: Number(m[2]), column: Number(m[3]) });
2611
2738
  }
2612
- return null;
2739
+ return frames;
2740
+ }
2741
+ function extractTopFrame(text) {
2742
+ return extractStackFrames(text)[0] ?? null;
2613
2743
  }
2614
2744
  function extractTopFrameFile(text) {
2615
2745
  return extractTopFrame(text)?.file ?? null;
@@ -3394,22 +3524,30 @@ function extractFailingLine(errorText, testFile2, declarationLine) {
3394
3524
  }
3395
3525
 
3396
3526
  // src/internal/support/ci.ts
3397
- function detectCiRunLabel() {
3398
- const env = process.env;
3399
- if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID) return env.GITHUB_RUN_ID;
3400
- if (env.GITLAB_CI && env.CI_PIPELINE_ID) return env.CI_PIPELINE_ID;
3401
- if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID) return env.CIRCLE_WORKFLOW_ID;
3402
- if (env.TRAVIS && env.TRAVIS_BUILD_ID) return env.TRAVIS_BUILD_ID;
3403
- if (env.TF_BUILD && env.BUILD_BUILDID) return env.BUILD_BUILDID;
3404
- if (env.JENKINS_URL && env.BUILD_ID) return env.BUILD_ID;
3405
- if (env.BUILDKITE_BUILD_ID) return env.BUILDKITE_BUILD_ID;
3406
- if (env.TEAMCITY_BUILD_ID) return env.TEAMCITY_BUILD_ID;
3407
- if (env.BITBUCKET_BUILD_NUMBER) return env.BITBUCKET_BUILD_NUMBER;
3408
- if (env.SEMAPHORE_WORKFLOW_ID) return env.SEMAPHORE_WORKFLOW_ID;
3409
- if (env.APPVEYOR_BUILD_ID) return env.APPVEYOR_BUILD_ID;
3410
- if (env.DRONE_BUILD_NUMBER) return env.DRONE_BUILD_NUMBER;
3527
+ function detectCiIds(env) {
3528
+ if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID) {
3529
+ const run = env.GITHUB_RUN_ATTEMPT ? `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` : env.GITHUB_RUN_ID;
3530
+ return { run, job: env.GITHUB_JOB || null };
3531
+ }
3532
+ if (env.GITLAB_CI && env.CI_PIPELINE_ID) return { run: env.CI_PIPELINE_ID, job: env.CI_JOB_ID || null };
3533
+ if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID) return { run: env.CIRCLE_WORKFLOW_ID, job: env.CIRCLE_BUILD_NUM || null };
3534
+ if (env.TRAVIS && env.TRAVIS_BUILD_ID) return { run: env.TRAVIS_BUILD_ID, job: env.TRAVIS_JOB_ID || null };
3535
+ if (env.TF_BUILD && env.BUILD_BUILDID) return { run: env.BUILD_BUILDID, job: env.SYSTEM_JOBID || null };
3536
+ if (env.JENKINS_URL && env.BUILD_ID) return { run: env.BUILD_ID, job: null };
3537
+ if (env.BUILDKITE_BUILD_ID) return { run: env.BUILDKITE_BUILD_ID, job: env.BUILDKITE_JOB_ID || null };
3538
+ if (env.TEAMCITY_BUILD_ID) return { run: env.TEAMCITY_BUILD_ID, job: null };
3539
+ if (env.BITBUCKET_BUILD_NUMBER) return { run: env.BITBUCKET_BUILD_NUMBER, job: env.BITBUCKET_STEP_UUID || null };
3540
+ if (env.SEMAPHORE_WORKFLOW_ID) return { run: env.SEMAPHORE_WORKFLOW_ID, job: env.SEMAPHORE_JOB_ID || null };
3541
+ if (env.APPVEYOR_BUILD_ID) return { run: env.APPVEYOR_BUILD_ID, job: env.APPVEYOR_JOB_ID || null };
3542
+ if (env.DRONE_BUILD_NUMBER) return { run: env.DRONE_BUILD_NUMBER, job: null };
3411
3543
  return null;
3412
3544
  }
3545
+ function resolveRunLabel(configured, sharded) {
3546
+ if (configured) return configured;
3547
+ const ci = detectCiIds(process.env);
3548
+ if (!ci) return null;
3549
+ return !sharded && ci.job ? `${ci.run}|${ci.job}` : ci.run;
3550
+ }
3413
3551
 
3414
3552
  // src/internal/support/worker-index.ts
3415
3553
  function workerIndexOf(result) {
@@ -3669,7 +3807,6 @@ function createGlobalSetup(options, userSetup) {
3669
3807
  const httpClient = new HttpClient(opts.serverUrl, logger);
3670
3808
  try {
3671
3809
  const auth = await httpClient.resolveAuth(opts);
3672
- const runLabel = opts.runLabel || detectCiRunLabel();
3673
3810
  const shard = resolveShardInfo(config);
3674
3811
  const response = await httpClient.postJSON(
3675
3812
  "/api/test-runs/setup",
@@ -3680,7 +3817,7 @@ function createGlobalSetup(options, userSetup) {
3680
3817
  label: opts.label || null,
3681
3818
  keep: opts.keep === true,
3682
3819
  startTime: (/* @__PURE__ */ new Date()).toISOString(),
3683
- instanceId: computeInstanceId(opts.projectName, runLabel),
3820
+ instanceId: computeInstanceId(opts.projectName, resolveRunLabel(opts.runLabel, shard !== null)),
3684
3821
  shardIndex: shard?.current,
3685
3822
  shardTotal: shard?.total
3686
3823
  },
@@ -3859,7 +3996,42 @@ var LONG_HEX_RE2 = /\b[0-9a-f]{8,}\b/gi;
3859
3996
  var SHORT_HEX_RE = /\b(?=[0-9a-f]*[a-f])(?=[0-9a-f]*[0-9])[0-9a-f]{6,7}\b/gi;
3860
3997
  var URL_RE2 = /\bhttps?:\/\/[^\s'"`)]+/gi;
3861
3998
  var EMAIL_RE = /\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b/gi;
3862
- var SELECTOR_OPTION_RE = /\b(name|hasText|hasNotText|has|placeholder|label|title|alt|exact)\s*:\s*(['"`])(?:\\.|(?!\2)[\s\S])*?\2/gi;
3999
+ var SELECTOR_OPTION_KEY_RE = /\b(name|hasText|hasNotText|has|placeholder|label|title|alt|exact)\s*:\s*(['"`])/gi;
4000
+ var LINE_TERMINATORS = /* @__PURE__ */ new Set(["\n", "\r", "\u2028", "\u2029"]);
4001
+ function closingQuoteIndex(text, quote) {
4002
+ const close = new Int32Array(text.length + 1).fill(-1);
4003
+ for (let p = text.length - 1; p >= 0; p--) {
4004
+ const ch = text[p];
4005
+ if (ch === quote) {
4006
+ close[p] = p;
4007
+ continue;
4008
+ }
4009
+ const escaped = ch === "\\" && p + 1 < text.length && !LINE_TERMINATORS.has(text[p + 1]) ? close[p + 2] : -1;
4010
+ close[p] = escaped !== -1 ? escaped : close[p + 1];
4011
+ }
4012
+ return close;
4013
+ }
4014
+ function maskSelectorOptions(text) {
4015
+ const closers = /* @__PURE__ */ new Map();
4016
+ let out = "";
4017
+ let copied = 0;
4018
+ SELECTOR_OPTION_KEY_RE.lastIndex = 0;
4019
+ let match;
4020
+ while (match = SELECTOR_OPTION_KEY_RE.exec(text)) {
4021
+ const quote = match[2];
4022
+ let close = closers.get(quote);
4023
+ if (!close) closers.set(quote, close = closingQuoteIndex(text, quote));
4024
+ const end = close[SELECTOR_OPTION_KEY_RE.lastIndex];
4025
+ if (end === -1) {
4026
+ SELECTOR_OPTION_KEY_RE.lastIndex = match.index + 1;
4027
+ continue;
4028
+ }
4029
+ out += `${text.slice(copied, match.index)}${match[1]}: <STR>`;
4030
+ copied = end + 1;
4031
+ SELECTOR_OPTION_KEY_RE.lastIndex = copied;
4032
+ }
4033
+ return out + text.slice(copied);
4034
+ }
3863
4035
  function classifyError(text) {
3864
4036
  if (/strict mode violation/i.test(text)) return "strict-mode";
3865
4037
  if (/\bexpect\(|\bexpect\.|Expected (?:string|substring|pattern|value)|\.toHave|\.toBe|\.toContain|\.toEqual/.test(text))
@@ -3873,9 +4045,6 @@ function classifyError(text) {
3873
4045
  function maskVolatile(text) {
3874
4046
  return text.replace(/^(\s*(?:Received|Expected)[^:\n]*:).*$/gm, "$1 <VALUE>").replace(URL_RE2, "<URL>").replace(EMAIL_RE, "<EMAIL>").replace(UUID_RE, "<UUID>").replace(LONG_HEX_RE2, "<HASH>").replace(SHORT_HEX_RE, "<HASH>").replace(/([A-Za-z])?(\d+)/g, (whole, letter) => letter ? whole : "<N>");
3875
4047
  }
3876
- function maskSelectorOptions(text) {
3877
- return text.replace(SELECTOR_OPTION_RE, (_m, key) => `${key}: <STR>`);
3878
- }
3879
4048
  function extractErrorSignature(rawError) {
3880
4049
  const text = stripAnsi(rawError);
3881
4050
  const errorType = classifyError(text);
@@ -4906,6 +5075,7 @@ function ciBuildUrlFromMetadata(metadata) {
4906
5075
  }
4907
5076
 
4908
5077
  // src/internal/submit/run-submitter.ts
5078
+ var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
4909
5079
  var RunSubmitter = class {
4910
5080
  /**
4911
5081
  * @param httpClient HTTP client for auth resolution and the finish call.
@@ -4923,8 +5093,28 @@ var RunSubmitter = class {
4923
5093
  this.logger = logger;
4924
5094
  this.failureLinks = failureLinks;
4925
5095
  }
4926
- /** Run the fallback ladder for a completed test run. */
5096
+ /**
5097
+ * Run the fallback ladder for a completed test run, within the
5098
+ * `submitTimeout` budget. When the budget runs out, every request in flight
5099
+ * and every later one fails, so the ladder falls through to saving the
5100
+ * recovery copy.
5101
+ */
4927
5102
  async submit(run, result) {
5103
+ const budgetMs = run.options.submitTimeout ?? 0;
5104
+ const timer = budgetMs > 0 && Number.isFinite(budgetMs) ? setTimeout(() => this.expireBudget(budgetMs), Math.min(budgetMs, MAX_TIMER_DELAY_MS)) : null;
5105
+ try {
5106
+ await this.submitRun(run, result);
5107
+ } finally {
5108
+ if (timer) clearTimeout(timer);
5109
+ }
5110
+ }
5111
+ expireBudget(budgetMs) {
5112
+ this.logger.warn(
5113
+ `The dashboard did not take the run within ${Math.round(budgetMs / 1e3)}s (submitTimeout) \u2014 stopping the upload and saving the results locally.`
5114
+ );
5115
+ this.httpClient.close(`the end-of-run time budget (submitTimeout: ${budgetMs} ms) ran out`);
5116
+ }
5117
+ async submitRun(run, result) {
4928
5118
  const endTime = (/* @__PURE__ */ new Date()).toISOString();
4929
5119
  const duration = new Date(endTime).getTime() - new Date(run.startTime).getTime();
4930
5120
  const counts = computeDistinctRunCounts(run.testCases);
@@ -4961,10 +5151,10 @@ var RunSubmitter = class {
4961
5151
  }
4962
5152
  if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
4963
5153
  let outcome = { done: false, output: null };
4964
- if (sm?.enabled && sm?.runId != null && !sm.bufferLostResults) {
5154
+ if (sm?.enabled && sm?.runId != null && !sm.lostResults) {
4965
5155
  outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
4966
5156
  }
4967
- if (!outcome.done && (this.hasReports(run) || run.options.uploadTraces)) {
5157
+ if (!outcome.done && !this.httpClient.closed && (this.hasReports(run) || run.options.uploadTraces)) {
4968
5158
  outcome = await this.tryUploadWithFiles(run, overallStatus, duration, auth);
4969
5159
  }
4970
5160
  if (!outcome.done) {
@@ -5059,7 +5249,7 @@ var RunSubmitter = class {
5059
5249
  }
5060
5250
  await this.httpClient.postJSON(`/api/test-runs/${sm.runId}/finish`, finishBody, auth);
5061
5251
  this.logger.info(`Successfully finalized streaming run #${sm.runId}`);
5062
- this.recovery.clear();
5252
+ this.dropLocalCopies();
5063
5253
  if (this.hasReports(run)) {
5064
5254
  try {
5065
5255
  await this.uploader.uploadReportsForStreamingRun(
@@ -5084,7 +5274,7 @@ var RunSubmitter = class {
5084
5274
  const payload = this.buildRunPayload(run, overallStatus, duration);
5085
5275
  try {
5086
5276
  const response = await this.uploader.uploadWithFiles(payload, this.reportOptions(run), auth);
5087
- this.recovery.clear();
5277
+ this.dropLocalCopies();
5088
5278
  return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
5089
5279
  } catch (error) {
5090
5280
  if (error instanceof HttpError && error.status === 401 && !auth) {
@@ -5101,7 +5291,7 @@ var RunSubmitter = class {
5101
5291
  const payload = this.buildRunPayload(run, overallStatus, duration);
5102
5292
  try {
5103
5293
  const response = await this.uploader.uploadJSON(payload, auth);
5104
- this.recovery.clear();
5294
+ this.dropLocalCopies();
5105
5295
  return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
5106
5296
  } catch (error) {
5107
5297
  if (error instanceof HttpError && error.status === 401 && !auth) {
@@ -5117,6 +5307,11 @@ var RunSubmitter = class {
5117
5307
  return { done: true, output: null };
5118
5308
  }
5119
5309
  }
5310
+ /** The run reached the server: drop the recovery copy and the run's buffered live events. */
5311
+ dropLocalCopies() {
5312
+ this.recovery.clear();
5313
+ this.streamManager?.discardBuffered();
5314
+ }
5120
5315
  /**
5121
5316
  * Persist the wire-serialized payload (no raw attachments / internal fields)
5122
5317
  * so a later run can retry the submit.
@@ -5294,7 +5489,7 @@ function countNoun(locator, count) {
5294
5489
  }
5295
5490
  function textOfGetByText(locator) {
5296
5491
  if (!locator) return null;
5297
- const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1).)*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
5492
+ const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
5298
5493
  return m ? m[2] : null;
5299
5494
  }
5300
5495
  function receivedDisplay(parsed) {
@@ -5679,7 +5874,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
5679
5874
  this.reportedTestIds = /* @__PURE__ */ new Set();
5680
5875
  /** Per-test attempt history, keyed by `test.id`; snapshotted onto every attempt's payload. */
5681
5876
  this.attemptsByTest = /* @__PURE__ */ new Map();
5682
- this.runLabel = null;
5877
+ /** Derived in `onBegin`, once the shard is known. */
5878
+ this.instanceId = "";
5683
5879
  this.shardInfo = null;
5684
5880
  this.metadata = {};
5685
5881
  this.isFullRun = true;
@@ -5693,8 +5889,6 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
5693
5889
  this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
5694
5890
  this.listMode = isListMode();
5695
5891
  this.viaDesktopApp = usedDesktopDiscovery();
5696
- this.runLabel = this.options.runLabel || detectCiRunLabel();
5697
- this.instanceId = computeInstanceId(this.options.projectName, this.runLabel);
5698
5892
  const logger = new Logger(this.options.verbose ?? false);
5699
5893
  this.logger = logger;
5700
5894
  this.httpClient = new HttpClient(this.options.serverUrl ?? "http://localhost:3000", logger);
@@ -5792,6 +5986,10 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
5792
5986
  if (this.shardInfo) {
5793
5987
  this.logger.info(`Shard ${this.shardInfo.current}/${this.shardInfo.total} detected`);
5794
5988
  }
5989
+ this.instanceId = computeInstanceId(
5990
+ this.options.projectName,
5991
+ resolveRunLabel(this.options.runLabel, this.shardInfo !== null)
5992
+ );
5795
5993
  this.streamManager?.start(
5796
5994
  this.startTime,
5797
5995
  this.metadata,
@@ -6515,7 +6713,7 @@ function installPickerOverlay(arg) {
6515
6713
  banner.style.cssText = `position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:${Z + 2};background:#111827;color:#f9fafb;font:13px/1.5 system-ui,sans-serif;border:1px solid #312e81;padding:10px 16px;border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,.5);max-width:min(680px,86vw);`;
6516
6714
  const hlTokens = (expr) => {
6517
6715
  const escHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6518
- const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
6716
+ const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
6519
6717
  let html = "";
6520
6718
  let last = 0;
6521
6719
  let m;
@@ -7058,7 +7256,7 @@ function showPickerChoices(arg) {
7058
7256
  panel.style.cssText = "background:#111827;color:#f9fafb;border-radius:10px;padding:20px;max-width:640px;width:90vw;max-height:70vh;overflow:auto;box-shadow:0 8px 40px rgba(0,0,0,.5);";
7059
7257
  const hlLocator = (expr) => {
7060
7258
  const escHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7061
- const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
7259
+ const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
7062
7260
  let html = "";
7063
7261
  let last = 0;
7064
7262
  let m;
@@ -8147,7 +8345,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
8147
8345
 
8148
8346
  // src/internal/capture/pick-on-failure.ts
8149
8347
  var path20 = __toESM(require("path"));
8150
- var ANSI_RE2 = /\[[0-9;]*m/g;
8348
+ var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
8151
8349
  function parseLeafLocatorExpression(rawExpr) {
8152
8350
  const leaf = parseLeafLocatorCall(rawExpr);
8153
8351
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -399,7 +399,7 @@ function installPickerOverlay(arg) {
399
399
  banner.style.cssText = `position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:${Z + 2};background:#111827;color:#f9fafb;font:13px/1.5 system-ui,sans-serif;border:1px solid #312e81;padding:10px 16px;border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,.5);max-width:min(680px,86vw);`;
400
400
  const hlTokens = (expr) => {
401
401
  const escHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
402
- const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
402
+ const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
403
403
  let html = "";
404
404
  let last = 0;
405
405
  let m;
@@ -942,7 +942,7 @@ function showPickerChoices(arg) {
942
942
  panel.style.cssText = "background:#111827;color:#f9fafb;border-radius:10px;padding:20px;max-width:640px;width:90vw;max-height:70vh;overflow:auto;box-shadow:0 8px 40px rgba(0,0,0,.5);";
943
943
  const hlLocator = (expr) => {
944
944
  const escHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
945
- const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
945
+ const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
946
946
  let html = "";
947
947
  let last = 0;
948
948
  let m;
@@ -2045,6 +2045,7 @@ var DEFAULTS = {
2045
2045
  streamingBatchSize: 5,
2046
2046
  streamingBatchDelay: 2e3,
2047
2047
  maxStreamBufferBytes: 100 * 1024 * 1024,
2048
+ submitTimeout: 15 * 60 * 1e3,
2048
2049
  failOnFlakyTests: false,
2049
2050
  username: null,
2050
2051
  password: null,
@@ -2066,6 +2067,7 @@ var PIWI_ENV_KEYS = {
2066
2067
  streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
2067
2068
  streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
2068
2069
  maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
2070
+ submitTimeout: "PIWI_SUBMIT_TIMEOUT",
2069
2071
  liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
2070
2072
  failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
2071
2073
  uploadTraces: "PIWI_UPLOAD_TRACES",
@@ -2115,6 +2117,7 @@ var ENV_FALLBACK_SPECS = [
2115
2117
  { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
2116
2118
  { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
2117
2119
  { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
2120
+ { option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
2118
2121
  { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
2119
2122
  { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
2120
2123
  { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
@@ -2791,15 +2794,19 @@ function extractSelector(text) {
2791
2794
  return text.slice(start, start + 80);
2792
2795
  }
2793
2796
  var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
2794
- function extractTopFrame(text) {
2797
+ function extractStackFrames(text) {
2795
2798
  const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
2799
+ const frames = [];
2796
2800
  let m;
2797
2801
  while ((m = frameRe.exec(text)) !== null) {
2798
2802
  const file = m[1].replace(/\\/g, "/");
2799
2803
  if (file.includes("node_modules") || file.startsWith("node:")) continue;
2800
- return { file, line: Number(m[2]), column: Number(m[3]) };
2804
+ frames.push({ file, line: Number(m[2]), column: Number(m[3]) });
2801
2805
  }
2802
- return null;
2806
+ return frames;
2807
+ }
2808
+ function extractTopFrame(text) {
2809
+ return extractStackFrames(text)[0] ?? null;
2803
2810
  }
2804
2811
  function extractTopFrameFile(text) {
2805
2812
  return extractTopFrame(text)?.file ?? null;
@@ -2813,7 +2820,42 @@ var LONG_HEX_RE = /\b[0-9a-f]{8,}\b/gi;
2813
2820
  var SHORT_HEX_RE = /\b(?=[0-9a-f]*[a-f])(?=[0-9a-f]*[0-9])[0-9a-f]{6,7}\b/gi;
2814
2821
  var URL_RE = /\bhttps?:\/\/[^\s'"`)]+/gi;
2815
2822
  var EMAIL_RE = /\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b/gi;
2816
- var SELECTOR_OPTION_RE = /\b(name|hasText|hasNotText|has|placeholder|label|title|alt|exact)\s*:\s*(['"`])(?:\\.|(?!\2)[\s\S])*?\2/gi;
2823
+ var SELECTOR_OPTION_KEY_RE = /\b(name|hasText|hasNotText|has|placeholder|label|title|alt|exact)\s*:\s*(['"`])/gi;
2824
+ var LINE_TERMINATORS = /* @__PURE__ */ new Set(["\n", "\r", "\u2028", "\u2029"]);
2825
+ function closingQuoteIndex(text, quote) {
2826
+ const close = new Int32Array(text.length + 1).fill(-1);
2827
+ for (let p = text.length - 1; p >= 0; p--) {
2828
+ const ch = text[p];
2829
+ if (ch === quote) {
2830
+ close[p] = p;
2831
+ continue;
2832
+ }
2833
+ const escaped = ch === "\\" && p + 1 < text.length && !LINE_TERMINATORS.has(text[p + 1]) ? close[p + 2] : -1;
2834
+ close[p] = escaped !== -1 ? escaped : close[p + 1];
2835
+ }
2836
+ return close;
2837
+ }
2838
+ function maskSelectorOptions(text) {
2839
+ const closers = /* @__PURE__ */ new Map();
2840
+ let out = "";
2841
+ let copied = 0;
2842
+ SELECTOR_OPTION_KEY_RE.lastIndex = 0;
2843
+ let match;
2844
+ while (match = SELECTOR_OPTION_KEY_RE.exec(text)) {
2845
+ const quote = match[2];
2846
+ let close = closers.get(quote);
2847
+ if (!close) closers.set(quote, close = closingQuoteIndex(text, quote));
2848
+ const end = close[SELECTOR_OPTION_KEY_RE.lastIndex];
2849
+ if (end === -1) {
2850
+ SELECTOR_OPTION_KEY_RE.lastIndex = match.index + 1;
2851
+ continue;
2852
+ }
2853
+ out += `${text.slice(copied, match.index)}${match[1]}: <STR>`;
2854
+ copied = end + 1;
2855
+ SELECTOR_OPTION_KEY_RE.lastIndex = copied;
2856
+ }
2857
+ return out + text.slice(copied);
2858
+ }
2817
2859
  function classifyError(text) {
2818
2860
  if (/strict mode violation/i.test(text)) return "strict-mode";
2819
2861
  if (/\bexpect\(|\bexpect\.|Expected (?:string|substring|pattern|value)|\.toHave|\.toBe|\.toContain|\.toEqual/.test(text))
@@ -2827,9 +2869,6 @@ function classifyError(text) {
2827
2869
  function maskVolatile(text) {
2828
2870
  return text.replace(/^(\s*(?:Received|Expected)[^:\n]*:).*$/gm, "$1 <VALUE>").replace(URL_RE, "<URL>").replace(EMAIL_RE, "<EMAIL>").replace(UUID_RE, "<UUID>").replace(LONG_HEX_RE, "<HASH>").replace(SHORT_HEX_RE, "<HASH>").replace(/([A-Za-z])?(\d+)/g, (whole, letter) => letter ? whole : "<N>");
2829
2871
  }
2830
- function maskSelectorOptions(text) {
2831
- return text.replace(SELECTOR_OPTION_RE, (_m, key) => `${key}: <STR>`);
2832
- }
2833
2872
  function extractErrorSignature(rawError) {
2834
2873
  const text = stripAnsi(rawError);
2835
2874
  const errorType = classifyError(text);
@@ -3216,7 +3255,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
3216
3255
 
3217
3256
  // src/internal/capture/pick-on-failure.ts
3218
3257
  var path5 = __toESM(require("path"));
3219
- var ANSI_RE2 = /\[[0-9;]*m/g;
3258
+ var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
3220
3259
  function parseLeafLocatorExpression(rawExpr) {
3221
3260
  const leaf = parseLeafLocatorCall(rawExpr);
3222
3261
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -85,12 +85,6 @@ declare const EXPECT_CAPTURE_EXPRESSIONS: ReadonlySet<string>;
85
85
  declare function extractAccessibleName(ariaSnapshot: string | null): string | null;
86
86
  /** Tags whose accessible name comes from an associated `<label>`. */
87
87
  declare const FORM_FIELD_TAGS: ReadonlySet<string>;
88
- /**
89
- * Roles whose accessible name is computed from the element's own content. For
90
- * these, an element carrying neither `aria-label` nor `aria-labelledby` is
91
- * named by its trimmed text — which the probe already returns.
92
- */
93
- declare const NAME_FROM_CONTENT_ROLES: ReadonlySet<string>;
94
88
  /** The subset of a probe result the accessible-name shortcut reads. */
95
89
  interface NameSource {
96
90
  tagName: string;
@@ -153,4 +147,4 @@ declare function suggestLocatorsFromAria(failed: FailedLocatorInfo, ariaSnapshot
153
147
  */
154
148
  declare function captureCallerLocation(stack?: string): string | null;
155
149
 
156
- export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, FORM_FIELD_TAGS, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, NAME_FROM_CONTENT_ROLES, type NameSource, captureCallerLocation, dedupeSnapshotsByLocation, exactAccessibleName, extractAccessibleName, renderFailing, suggestLocatorsFromAria };
150
+ export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, FORM_FIELD_TAGS, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, type NameSource, captureCallerLocation, dedupeSnapshotsByLocation, exactAccessibleName, extractAccessibleName, renderFailing, suggestLocatorsFromAria };
@@ -39,7 +39,6 @@ __export(locator_healing_exports, {
39
39
  INPUT_TYPE_TO_ROLE: () => INPUT_TYPE_TO_ROLE,
40
40
  LOCATOR_CREATING_CHAINS: () => LOCATOR_CREATING_CHAINS,
41
41
  LOCATOR_METHODS: () => LOCATOR_METHODS,
42
- NAME_FROM_CONTENT_ROLES: () => NAME_FROM_CONTENT_ROLES,
43
42
  TAG_TO_ROLE: () => TAG_TO_ROLE,
44
43
  approximateAccessibleName: () => approximateAccessibleName,
45
44
  captureCallerLocation: () => captureCallerLocation,
@@ -760,7 +759,6 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
760
759
  INPUT_TYPE_TO_ROLE,
761
760
  LOCATOR_CREATING_CHAINS,
762
761
  LOCATOR_METHODS,
763
- NAME_FROM_CONTENT_ROLES,
764
762
  TAG_TO_ROLE,
765
763
  approximateAccessibleName,
766
764
  captureCallerLocation,
@@ -343,7 +343,7 @@ function installPickerOverlay(arg) {
343
343
  banner.style.cssText = `position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:${Z + 2};background:#111827;color:#f9fafb;font:13px/1.5 system-ui,sans-serif;border:1px solid #312e81;padding:10px 16px;border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,.5);max-width:min(680px,86vw);`;
344
344
  const hlTokens = (expr) => {
345
345
  const escHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
346
- const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
346
+ const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
347
347
  let html = "";
348
348
  let last = 0;
349
349
  let m;
@@ -886,7 +886,7 @@ function showPickerChoices(arg) {
886
886
  panel.style.cssText = "background:#111827;color:#f9fafb;border-radius:10px;padding:20px;max-width:640px;width:90vw;max-height:70vh;overflow:auto;box-shadow:0 8px 40px rgba(0,0,0,.5);";
887
887
  const hlLocator = (expr) => {
888
888
  const escHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
889
- const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
889
+ const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
890
890
  let html = "";
891
891
  let last = 0;
892
892
  let m;
@@ -1412,7 +1412,7 @@ function renderFailing(failed) {
1412
1412
  }
1413
1413
 
1414
1414
  // src/internal/capture/pick-on-failure.ts
1415
- var ANSI_RE = /\[[0-9;]*m/g;
1415
+ var ANSI_RE = /\x1b\[[0-9;]*m/g;
1416
1416
  function parseLeafLocatorExpression(rawExpr) {
1417
1417
  const leaf = parseLeafLocatorCall(rawExpr);
1418
1418
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piwitests/reporter",
3
- "version": "0.42.0",
3
+ "version": "0.43.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",