@piwitests/reporter 0.55.0 → 0.57.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/index.js CHANGED
@@ -45,7 +45,7 @@ module.exports = __toCommonJS(index_exports);
45
45
  // src/public/reporter.ts
46
46
  var fs24 = __toESM(require("fs"));
47
47
  var os9 = __toESM(require("os"));
48
- var path26 = __toESM(require("path"));
48
+ var path24 = __toESM(require("path"));
49
49
 
50
50
  // src/internal/config/desktop.ts
51
51
  var fs = __toESM(require("fs"));
@@ -363,11 +363,15 @@ var HttpClient = class {
363
363
  * @param logger Prefixed logger for verbose diagnostics.
364
364
  * @param timeout Socket inactivity timeout in ms (default 30s), so a hung
365
365
  * server fails fast rather than stalling the reporter.
366
+ * @param uploadTimeout Socket inactivity timeout of a multipart upload in ms
367
+ * (default 10 min): the dashboard answers one only once it
368
+ * has stored every report and trace in it.
366
369
  */
367
- constructor(serverUrl, logger = new Logger(), timeout = 3e4) {
370
+ constructor(serverUrl, logger = new Logger(), timeout = 3e4, uploadTimeout = 10 * 60 * 1e3) {
368
371
  this.serverUrl = serverUrl;
369
372
  this.logger = logger;
370
373
  this.timeout = timeout;
374
+ this.uploadTimeout = uploadTimeout;
371
375
  /** Requests in flight, so `close` can abort them. */
372
376
  this.inFlight = /* @__PURE__ */ new Set();
373
377
  /** Set by `close`: every later request fails with this reason. */
@@ -515,7 +519,7 @@ var HttpClient = class {
515
519
  /** Send a multipart form-data POST request. Used for report and trace uploads. */
516
520
  async postFormData(pathname, form, auth) {
517
521
  const headers = form.getHeaders();
518
- const res = await this.request("POST", pathname, { headers, form, auth });
522
+ const res = await this.request("POST", pathname, { headers, form, auth, timeout: this.uploadTimeout });
519
523
  if (res.status < 200 || res.status >= 300) {
520
524
  this.logger.debugError(`Response: ${res.text}`);
521
525
  const detail = parseErrorMessage(res.text);
@@ -568,8 +572,9 @@ var HttpClient = class {
568
572
  this.inFlight.add(req);
569
573
  req.on("close", () => this.inFlight.delete(req));
570
574
  req.on("error", reject);
571
- req.setTimeout(this.timeout, () => {
572
- req.destroy(new Error(`Request to ${pathname} timed out after ${this.timeout}ms`));
575
+ const timeout = opts.timeout ?? this.timeout;
576
+ req.setTimeout(timeout, () => {
577
+ req.destroy(new Error(`Request to ${pathname} timed out after ${timeout}ms`));
573
578
  });
574
579
  if (opts.form) {
575
580
  opts.form.on("error", (error) => req.destroy(error));
@@ -663,13 +668,18 @@ function browserProjectName(browser) {
663
668
  }
664
669
  return "";
665
670
  }
671
+ function isFailingStatus(status) {
672
+ return status === "failed" || status === "timedOut" || status === "timedout" || status === "interrupted";
673
+ }
666
674
  function computeDistinctRunCounts(testCases) {
667
675
  const final = /* @__PURE__ */ new Map();
668
676
  for (const tc of testCases) {
669
- const key = `${tc.title}\0${tc.location}\0${browserProjectName(tc.browser)}`;
677
+ const key = tc.testId ?? [tc.title, tc.location, (tc.suitePath ?? []).join(""), browserProjectName(tc.browser)].join("\0");
678
+ const status = tc.status ?? "";
670
679
  const retries = tc.retries ?? 0;
671
680
  const prev = final.get(key);
672
- if (!prev || retries >= prev.retries) final.set(key, { status: tc.status ?? "", retries });
681
+ const decides = !prev || retries > prev.retries || retries === prev.retries && (isFailingStatus(status) || !isFailingStatus(prev.status));
682
+ if (decides) final.set(key, { status, retries });
673
683
  }
674
684
  const counts = {
675
685
  totalTests: final.size,
@@ -677,14 +687,11 @@ function computeDistinctRunCounts(testCases) {
677
687
  failedTests: 0,
678
688
  timedOutTests: 0,
679
689
  skippedTests: 0,
680
- didNotRunTests: 0,
681
- flakyTests: 0
690
+ didNotRunTests: 0
682
691
  };
683
- for (const { status, retries } of final.values()) {
684
- if (status === "passed") {
685
- counts.passedTests++;
686
- if (retries > 0) counts.flakyTests++;
687
- } else if (status === "failed") counts.failedTests++;
692
+ for (const { status } of final.values()) {
693
+ if (status === "passed") counts.passedTests++;
694
+ else if (status === "failed") counts.failedTests++;
688
695
  else if (status === "timedOut" || status === "timedout") counts.timedOutTests++;
689
696
  else if (status === "skipped") counts.skippedTests++;
690
697
  else if (status === "didnotrun") counts.didNotRunTests++;
@@ -719,6 +726,7 @@ function serializeRun(payload, opts) {
719
726
  filterDetails: payload.filterDetails ?? null
720
727
  };
721
728
  if (payload.resourceReport) body.resourceReport = payload.resourceReport;
729
+ if (payload.deliveryKey) body.deliveryKey = payload.deliveryKey;
722
730
  if (opts.includeTestCases) {
723
731
  body.testCases = payload.testCases.map((tc) => toWireTestCase(tc));
724
732
  }
@@ -990,6 +998,9 @@ var StreamBuffer = class {
990
998
  };
991
999
 
992
1000
  // src/internal/streaming/crash-recovery.ts
1001
+ var crypto2 = __toESM(require("crypto"));
1002
+ var fs4 = __toESM(require("fs"));
1003
+ var os4 = __toESM(require("os"));
993
1004
  var path4 = __toESM(require("path"));
994
1005
 
995
1006
  // src/internal/support/errors.ts
@@ -999,45 +1010,79 @@ function errorMessage(error) {
999
1010
  }
1000
1011
 
1001
1012
  // src/internal/streaming/crash-recovery.ts
1002
- var os4 = __toESM(require("os"));
1003
- var fs4 = __toESM(require("fs"));
1013
+ function recoveryServer(serverUrl) {
1014
+ return serverUrl.replace(/\/+$/, "");
1015
+ }
1016
+ function recoveryFilePath(serverUrl, projectName) {
1017
+ const key = crypto2.createHash("sha256").update(`${recoveryServer(serverUrl)}
1018
+ ${projectName}`).digest("hex").slice(0, 16);
1019
+ return path4.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
1020
+ }
1021
+ function refusesRun(error) {
1022
+ return error instanceof HttpError && [400, 409, 422].includes(error.status);
1023
+ }
1004
1024
  var CrashRecovery = class {
1005
1025
  /**
1006
- * @param projectName Used to derive the temp-file name so recovery data is project-scoped.
1026
+ * @param serverUrl The dashboard the run is for: the file is kept for it and replayed to it only.
1027
+ * @param projectName With `serverUrl`, the key of the file.
1007
1028
  * @param logger Prefixed logger.
1008
1029
  */
1009
- constructor(projectName, logger = new Logger()) {
1030
+ constructor(serverUrl, projectName, logger = new Logger()) {
1010
1031
  this.logger = logger;
1011
- this.filePath = path4.join(os4.tmpdir(), `piwi-dashboard-recovery-${hashForProject(projectName)}.json`);
1032
+ this.serverUrl = recoveryServer(serverUrl);
1033
+ this.filePath = recoveryFilePath(serverUrl, projectName);
1012
1034
  }
1013
- /** Serialise the payload to a temp file for later retry */
1014
- save(data) {
1035
+ /** Write the payload for a later retry, owner-only, replacing the file in one step. */
1036
+ save(payload) {
1037
+ const file = { v: 1, serverUrl: this.serverUrl, payload };
1038
+ const temp = `${this.filePath}.${process.pid}.${crypto2.randomBytes(6).toString("hex")}.tmp`;
1015
1039
  try {
1016
- fs4.writeFileSync(this.filePath, JSON.stringify(data), "utf8");
1040
+ fs4.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
1041
+ fs4.renameSync(temp, this.filePath);
1017
1042
  this.logger.info("Saved recovery data for later upload");
1018
1043
  } catch (error) {
1044
+ fs4.rmSync(temp, { force: true });
1019
1045
  this.logger.error(`Failed to save recovery data: ${errorMessage(error)}`);
1020
1046
  }
1021
1047
  }
1022
- /** Read the saved payload from disk, or return `null` if none exists */
1048
+ /**
1049
+ * The saved payload, or null when there is none, or when the file is not a
1050
+ * regular file this user owns, or names another dashboard.
1051
+ */
1023
1052
  load() {
1053
+ let fd;
1024
1054
  try {
1025
- if (fs4.existsSync(this.filePath)) return JSON.parse(fs4.readFileSync(this.filePath, "utf8"));
1055
+ fd = fs4.openSync(this.filePath, fs4.constants.O_RDONLY | (fs4.constants.O_NOFOLLOW ?? 0));
1026
1056
  } catch {
1057
+ return null;
1058
+ }
1059
+ try {
1060
+ const stat = fs4.fstatSync(fd);
1061
+ if (!stat.isFile() || typeof process.getuid === "function" && stat.uid !== process.getuid()) return null;
1062
+ const file = JSON.parse(fs4.readFileSync(fd, "utf8"));
1063
+ if (file?.v !== 1 || file.serverUrl !== this.serverUrl) return null;
1064
+ return file.payload && typeof file.payload === "object" ? file.payload : null;
1065
+ } catch {
1066
+ return null;
1067
+ } finally {
1068
+ fs4.closeSync(fd);
1027
1069
  }
1028
- return null;
1029
1070
  }
1030
1071
  /** Delete the recovery file from disk */
1031
1072
  clear() {
1032
1073
  try {
1033
- if (fs4.existsSync(this.filePath)) fs4.unlinkSync(this.filePath);
1074
+ fs4.rmSync(this.filePath, { force: true });
1034
1075
  } catch {
1035
1076
  }
1036
1077
  }
1037
- /** Attempt to submit a previously saved payload. Clears the recovery file on success. */
1078
+ /**
1079
+ * Submit a previously saved payload to the dashboard it was saved for. The
1080
+ * file is deleted once the dashboard took the run, or refused it for good;
1081
+ * anything else keeps it for the next run.
1082
+ */
1038
1083
  async tryUpload(httpClient, auth) {
1039
1084
  const data = this.load();
1040
- if (!data) return;
1085
+ if (!data || recoveryServer(httpClient.baseUrl) !== this.serverUrl) return;
1041
1086
  this.logger.info("Found saved test data from a previous run, uploading...");
1042
1087
  delete data.instanceId;
1043
1088
  const metadata = data.metadata && typeof data.metadata === "object" ? data.metadata : {};
@@ -1047,6 +1092,11 @@ var CrashRecovery = class {
1047
1092
  this.logger.info("Successfully uploaded saved test data");
1048
1093
  this.clear();
1049
1094
  } catch (error) {
1095
+ if (refusesRun(error)) {
1096
+ this.logger.warn(`The dashboard refused the saved test data, which is deleted: ${errorMessage(error)}`);
1097
+ this.clear();
1098
+ return;
1099
+ }
1050
1100
  this.logger.warn(`Could not upload saved test data: ${errorMessage(error)}`);
1051
1101
  }
1052
1102
  }
@@ -1056,7 +1106,7 @@ var CrashRecovery = class {
1056
1106
  var fs6 = __toESM(require("fs"));
1057
1107
  var os5 = __toESM(require("os"));
1058
1108
  var path6 = __toESM(require("path"));
1059
- var crypto2 = __toESM(require("crypto"));
1109
+ var crypto3 = __toESM(require("crypto"));
1060
1110
 
1061
1111
  // src/internal/files/compression.ts
1062
1112
  var fs5 = __toESM(require("fs"));
@@ -1355,7 +1405,7 @@ var FileHandler = class {
1355
1405
  if (fs6.existsSync(tp)) lastPath = tp;
1356
1406
  }
1357
1407
  if (!lastPath) return null;
1358
- const hash = crypto2.createHash("sha256");
1408
+ const hash = crypto3.createHash("sha256");
1359
1409
  await new Promise((resolve9, reject) => {
1360
1410
  fs6.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve9).on("error", reject);
1361
1411
  });
@@ -1387,6 +1437,31 @@ function formatMiB(bytes) {
1387
1437
 
1388
1438
  // src/internal/collect/metadata-collector.ts
1389
1439
  var import_node_child_process = require("child_process");
1440
+
1441
+ // src/internal/support/test-paths.ts
1442
+ var path7 = __toESM(require("path"));
1443
+ function configDirFromConfig(config) {
1444
+ if (config?.configFile) return path7.dirname(config.configFile);
1445
+ if (config?.rootDir) return config.rootDir;
1446
+ return process.cwd();
1447
+ }
1448
+ function relativeTestPath(file, base) {
1449
+ return path7.relative(base, file).split(path7.sep).join("/");
1450
+ }
1451
+
1452
+ // src/internal/collect/metadata-collector.ts
1453
+ function withoutCredentials(remoteUrl) {
1454
+ let url;
1455
+ try {
1456
+ url = new URL(remoteUrl);
1457
+ } catch {
1458
+ return remoteUrl;
1459
+ }
1460
+ if (!url.username && !url.password) return remoteUrl;
1461
+ url.username = "";
1462
+ url.password = "";
1463
+ return url.toString();
1464
+ }
1390
1465
  var DETACHED_HEAD = "HEAD";
1391
1466
  function resolveScmBranch(env, gitBranch) {
1392
1467
  const pick = (...names) => {
@@ -1454,7 +1529,7 @@ var MetadataCollector = class {
1454
1529
  /** Collect all available metadata from the environment, config, and suite */
1455
1530
  collect(config, suite, options) {
1456
1531
  const metadata = {
1457
- workingDir: process.cwd(),
1532
+ workingDir: configDirFromConfig(config),
1458
1533
  [RUN_ORIGIN_METADATA_KEY]: resolveRunOrigin(process.env, (message) => this.logger.warn(message))
1459
1534
  };
1460
1535
  if (options.projectDescription) metadata.projectDescription = options.projectDescription;
@@ -1572,7 +1647,7 @@ var MetadataCollector = class {
1572
1647
  scm.author = (0, import_node_child_process.execSync)('git log -1 --pretty=format:"%an"', execOpts).trim();
1573
1648
  scm.commitMessage = (0, import_node_child_process.execSync)('git log -1 --pretty=format:"%s"', execOpts).trim();
1574
1649
  try {
1575
- scm.remoteUrl = (0, import_node_child_process.execSync)("git config --get remote.origin.url", execOpts).trim();
1650
+ scm.remoteUrl = withoutCredentials((0, import_node_child_process.execSync)("git config --get remote.origin.url", execOpts).trim());
1576
1651
  } catch {
1577
1652
  }
1578
1653
  } catch (error) {
@@ -1825,11 +1900,11 @@ function createLimiter(maxConcurrent) {
1825
1900
  }
1826
1901
 
1827
1902
  // src/internal/support/setup-file.ts
1828
- var path7 = __toESM(require("path"));
1903
+ var path8 = __toESM(require("path"));
1829
1904
  var os6 = __toESM(require("os"));
1830
1905
  var fs7 = __toESM(require("fs"));
1831
1906
  function getSetupFilePath(projectName) {
1832
- return path7.join(os6.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
1907
+ return path8.join(os6.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
1833
1908
  }
1834
1909
  function readSetupInfo(projectName) {
1835
1910
  const setupFile = getSetupFilePath(projectName);
@@ -2678,6 +2753,10 @@ function tryParseLocatorChain(expr) {
2678
2753
  return null;
2679
2754
  }
2680
2755
  }
2756
+ function quoteJsString(value) {
2757
+ const json = JSON.stringify(value).slice(1, -1);
2758
+ return `'${json.replace(/\\"/g, '"').replace(/'/g, "\\'").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
2759
+ }
2681
2760
  function parseLeafLocatorCall(expr) {
2682
2761
  const chain = tryParseLocatorChain(expr);
2683
2762
  if (!chain) return null;
@@ -2744,11 +2823,20 @@ function extractMessageHead(text) {
2744
2823
  let head = text;
2745
2824
  const callLogIdx = head.indexOf("\nCall log:");
2746
2825
  if (callLogIdx !== -1) head = head.slice(0, callLogIdx);
2747
- const stackIdx = head.search(/\n\s+at /);
2826
+ const stackIdx = stackStart(head);
2748
2827
  if (stackIdx !== -1) head = head.slice(0, stackIdx);
2749
2828
  const lines = head.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
2750
2829
  return lines.slice(0, 5).join("\n");
2751
2830
  }
2831
+ function stackStart(text) {
2832
+ const runs = /\s+/g;
2833
+ for (let run = runs.exec(text); run; run = runs.exec(text)) {
2834
+ if (!text.startsWith("at ", runs.lastIndex)) continue;
2835
+ const lineBreak = run[0].indexOf("\n");
2836
+ if (lineBreak !== -1 && lineBreak < run[0].length - 1) return run.index + lineBreak;
2837
+ }
2838
+ return -1;
2839
+ }
2752
2840
  function extractSelector(text) {
2753
2841
  const match = SELECTOR_FN_RE.exec(text);
2754
2842
  if (!match) return null;
@@ -2869,7 +2957,7 @@ function readChain(region) {
2869
2957
  return link && CHAIN_LINK_METHODS.has(link[1]) ? null : scanned;
2870
2958
  }
2871
2959
  function extractStackFrames(text) {
2872
- const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
2960
+ const frameRe = /^[ \t]+at (?:.*? \()?([^()\s][^()\n]*?):(\d+):(\d+)\)?[ \t]*$/gm;
2873
2961
  const frames = [];
2874
2962
  let m;
2875
2963
  while ((m = frameRe.exec(text)) !== null) {
@@ -2992,21 +3080,25 @@ function readCallLog(text) {
2992
3080
  return read;
2993
3081
  }
2994
3082
  function headerValue(text, label) {
2995
- const re = new RegExp(`^\\s*${label}(?: string| pattern| substring| value)?:[ \\t]*(.*)$`, "m");
3083
+ const re = new RegExp(`^[ \\t]*${label}(?: string| pattern| substring| value)?:[ \\t]*(.*)$`, "m");
2996
3084
  const m = re.exec(text);
2997
3085
  if (!m) return null;
2998
3086
  const value = m[1].trim();
2999
3087
  return value.length > 0 ? value : null;
3000
3088
  }
3089
+ function readLocatorHeader(text) {
3090
+ const header = /^[ \t]*Locator:[ \t]*(.+)$/m.exec(text);
3091
+ return header ? header[1].trim() : null;
3092
+ }
3001
3093
  function readLocator(text, strict) {
3002
- const header = /^\s*Locator:[ \t]*(.+)$/m.exec(text);
3003
- if (header) return header[1].trim();
3094
+ const header = readLocatorHeader(text);
3095
+ if (header) return header;
3004
3096
  if (strict) return strict[1].trim();
3005
3097
  return extractLocatorChain(text);
3006
3098
  }
3007
3099
  function readTimeout(text, callLog) {
3008
3100
  const patterns = [
3009
- /^\s*Timeout:[ \t]*(\d+)ms/m,
3101
+ /^[ \t]*Timeout:[ \t]*(\d+)ms/m,
3010
3102
  /\bTimeout (\d+)ms exceeded/,
3011
3103
  TEST_TIMEOUT_RE,
3012
3104
  TIMED_OUT_EXPECT_RE,
@@ -3023,7 +3115,7 @@ function readUrl(text, callLog, received) {
3023
3115
  const at = /\b(?:net::ERR_[A-Z0-9_]+|NS_ERROR_[A-Z0-9_]+) at (\S+)/.exec(text);
3024
3116
  if (at) return at[1];
3025
3117
  const head = extractMessageHead(text);
3026
- const inHead = URL_RE.exec(head.replace(/^\s*Received.*$/gm, ""));
3118
+ const inHead = URL_RE.exec(head.replace(/^[ \t]*Received.*$/gm, ""));
3027
3119
  if (inHead) return inHead[0];
3028
3120
  if (received) {
3029
3121
  const inReceived = URL_RE.exec(received);
@@ -3039,8 +3131,9 @@ function readCustomMessage(messageHead) {
3039
3131
  if (!first || MATCHER_LINE_RE.test(first)) return null;
3040
3132
  return /^(?:expect[.(]|Timed out\b)/.test(lines[1]) ? first : null;
3041
3133
  }
3134
+ var MAX_PARSED_ERROR_CHARS = 64 * 1024;
3042
3135
  function parsePlaywrightError(raw, context) {
3043
- const clean = stripAnsi(raw ?? "").replace(/\r\n?/g, "\n");
3136
+ const clean = stripAnsi((raw ?? "").slice(0, MAX_PARSED_ERROR_CHARS)).replace(/\r\n?/g, "\n");
3044
3137
  const text = withoutStackFrames(clean);
3045
3138
  const messageHead = extractMessageHead(clean);
3046
3139
  const firstLine2 = messageHead.split("\n")[0] ?? "";
@@ -3077,7 +3170,7 @@ function parsePlaywrightError(raw, context) {
3077
3170
  else if (testTimeout) kind = "test-timeout";
3078
3171
  else if (CRASH_RE.test(text)) kind = "crash";
3079
3172
  else if (isAssertion) {
3080
- const retrying = /^\s*Timeout:[ \t]*\d+ms/m.test(text) || TIMED_OUT_EXPECT_RE.test(text) || callLog.matcher !== null;
3173
+ const retrying = /^[ \t]*Timeout:[ \t]*\d+ms/m.test(text) || TIMED_OUT_EXPECT_RE.test(text) || callLog.matcher !== null;
3081
3174
  kind = retrying ? "assertion-timeout" : "assertion";
3082
3175
  } else if (isNavigationFailure) kind = "navigation";
3083
3176
  else if (/\bTimeout \d+ms exceeded/.test(text) || errorName === "TimeoutError") kind = "action-timeout";
@@ -3128,9 +3221,25 @@ var LONG_HEX_RE = /\b[0-9a-f]{32,}\b/gi;
3128
3221
  function maskTokenLike(text) {
3129
3222
  return text.replace(DATA_URI_RE, "data:[masked]").replace(JWT_RE, "[masked-token]").replace(LONG_HEX_RE, "[masked-hex]");
3130
3223
  }
3224
+ var CREDENTIAL_PARAM_RE = /token|code|secret|password|passwd|pwd|key|session|auth|sig|credential|assertion|nonce|otp|jwt/i;
3225
+ function maskUrlFragment(hash) {
3226
+ const body = hash.startsWith("#") ? hash.slice(1) : hash;
3227
+ const query = body.indexOf("?");
3228
+ const route = query >= 0 ? body.slice(0, query) : body.includes("=") ? "" : body;
3229
+ const params = query >= 0 ? body.slice(query + 1) : body.includes("=") ? body : null;
3230
+ const masked = params === null ? null : params.split("&").map((param) => {
3231
+ const eq = param.indexOf("=");
3232
+ if (eq < 0) return maskTokenLike(param);
3233
+ const name = param.slice(0, eq);
3234
+ const value = param.slice(eq + 1);
3235
+ return `${name}=${value && CREDENTIAL_PARAM_RE.test(name) ? "[masked]" : maskTokenLike(value)}`;
3236
+ }).join("&");
3237
+ const out = maskTokenLike(route) + (masked === null ? "" : query >= 0 ? `?${masked}` : masked);
3238
+ return hash.startsWith("#") ? `#${out}` : out;
3239
+ }
3131
3240
 
3132
3241
  // ../core/src/locator-break.ts
3133
- var filePath = (path34) => path34.replace(/\\/g, "/").replace(/^\.\//, "");
3242
+ var filePath = (path32) => path32.replace(/\\/g, "/").replace(/^\.\//, "");
3134
3243
  function sameFilePath(a, b) {
3135
3244
  const x = filePath(a);
3136
3245
  const y = filePath(b);
@@ -3559,17 +3668,17 @@ function extractWaitEvents(steps, insideWait = false) {
3559
3668
 
3560
3669
  // src/internal/support/reporter-version.ts
3561
3670
  var fs9 = __toESM(require("fs"));
3562
- var path8 = __toESM(require("path"));
3671
+ var path9 = __toESM(require("path"));
3563
3672
  var cachedVersion = null;
3564
3673
  var CANDIDATE_OFFSETS = ["..", "../../.."];
3565
3674
  function findOwnPackageJson(fromDir) {
3566
3675
  for (const offset of CANDIDATE_OFFSETS) {
3567
3676
  try {
3568
- const pkgPath = path8.resolve(fromDir, offset, "package.json");
3677
+ const pkgPath = path9.resolve(fromDir, offset, "package.json");
3569
3678
  const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
3570
3679
  const { name, version } = pkg;
3571
3680
  if (name === "@piwitests/reporter" && typeof version === "string")
3572
- return { version, root: path8.dirname(pkgPath) };
3681
+ return { version, root: path9.dirname(pkgPath) };
3573
3682
  } catch {
3574
3683
  }
3575
3684
  }
@@ -3583,7 +3692,7 @@ function getReporterVersion() {
3583
3692
 
3584
3693
  // src/internal/support/source-snippet.ts
3585
3694
  var fs10 = __toESM(require("fs"));
3586
- var path9 = __toESM(require("path"));
3695
+ var path10 = __toESM(require("path"));
3587
3696
  function readSourceSnippet(file, declLine, context, failingLine) {
3588
3697
  try {
3589
3698
  const content = fs10.readFileSync(file, "utf-8");
@@ -3605,6 +3714,10 @@ function readSourceSnippet(file, declLine, context, failingLine) {
3605
3714
  return null;
3606
3715
  }
3607
3716
  }
3717
+ var MAX_SCANNED_ERROR_CHARS = 64 * 1024;
3718
+ function stackFrames(errorText) {
3719
+ return extractStackFrames(errorText.slice(0, MAX_SCANNED_ERROR_CHARS));
3720
+ }
3608
3721
  function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
3609
3722
  const projectRoot = opts.projectRoot ?? process.cwd();
3610
3723
  const context = opts.context ?? 8;
@@ -3618,23 +3731,20 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
3618
3731
  picked.push({ absFile, line });
3619
3732
  };
3620
3733
  if (errorText) {
3621
- const stackRe = /^\s+at (?:[^(]*\()?(.+?):(\d+):\d+\)?\s*$/gm;
3622
- let m;
3623
- while ((m = stackRe.exec(errorText)) !== null && picked.length < maxFrames) {
3734
+ for (const frame of stackFrames(errorText)) {
3735
+ if (picked.length >= maxFrames) break;
3624
3736
  let abs;
3625
3737
  try {
3626
- abs = path9.resolve(m[1]);
3738
+ abs = path10.resolve(frame.file);
3627
3739
  } catch {
3628
3740
  continue;
3629
3741
  }
3630
- if (abs.includes(`${path9.sep}node_modules${path9.sep}`)) continue;
3631
- const rel = path9.relative(projectRoot, abs);
3632
- if (rel.startsWith("..") || path9.isAbsolute(rel)) continue;
3633
- const line = parseInt(m[2], 10);
3634
- if (!isNaN(line)) add(abs, line);
3742
+ const rel = path10.relative(projectRoot, abs);
3743
+ if (rel.startsWith("..") || path10.isAbsolute(rel)) continue;
3744
+ if (!isNaN(frame.line)) add(abs, frame.line);
3635
3745
  }
3636
3746
  }
3637
- const absTest = path9.resolve(testFile2);
3747
+ const absTest = path10.resolve(testFile2);
3638
3748
  if (!picked.some((p) => p.absFile === absTest)) {
3639
3749
  add(absTest, extractFailingLine(errorText, testFile2, declLine));
3640
3750
  }
@@ -3642,23 +3752,17 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
3642
3752
  for (const p of picked.slice(0, maxFrames)) {
3643
3753
  const snippet = readSourceSnippet(p.absFile, p.line, context, p.line);
3644
3754
  if (snippet) {
3645
- frames.push({ file: path9.relative(projectRoot, p.absFile) || path9.basename(p.absFile), line: p.line, snippet });
3755
+ frames.push({ file: path10.relative(projectRoot, p.absFile) || path10.basename(p.absFile), line: p.line, snippet });
3646
3756
  }
3647
3757
  }
3648
3758
  return frames;
3649
3759
  }
3650
3760
  function extractFailingLine(errorText, testFile2, declarationLine) {
3651
3761
  if (!errorText) return declarationLine;
3652
- const expectedFile = path9.resolve(testFile2);
3653
- const stackRe = /^\s+at (?:[^(]*\()?(.+?):(\d+):\d+\)?\s*$/gm;
3654
- let m;
3655
- while ((m = stackRe.exec(errorText)) !== null) {
3762
+ const expectedFile = path10.resolve(testFile2);
3763
+ for (const frame of stackFrames(errorText)) {
3656
3764
  try {
3657
- const frameFile = path9.resolve(m[1]);
3658
- if (frameFile === expectedFile) {
3659
- const line = parseInt(m[2], 10);
3660
- if (!isNaN(line)) return line;
3661
- }
3765
+ if (path10.resolve(frame.file) === expectedFile && !isNaN(frame.line)) return frame.line;
3662
3766
  } catch {
3663
3767
  }
3664
3768
  }
@@ -3776,18 +3880,18 @@ function isListMode(argv = process.argv) {
3776
3880
  }
3777
3881
 
3778
3882
  // src/public/global-setup.ts
3779
- var path13 = __toESM(require("path"));
3883
+ var path14 = __toESM(require("path"));
3780
3884
  var fs14 = __toESM(require("fs"));
3781
3885
 
3782
3886
  // src/internal/support/aria-sampling.ts
3783
- var path10 = __toESM(require("path"));
3887
+ var path11 = __toESM(require("path"));
3784
3888
  var os7 = __toESM(require("os"));
3785
3889
  var fs11 = __toESM(require("fs"));
3786
3890
  function ariaSampleIdentity(filePath2, title) {
3787
3891
  return `${filePath2}\0${title}`;
3788
3892
  }
3789
3893
  function getAriaSampleFilePath(projectName) {
3790
- return path10.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
3894
+ return path11.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
3791
3895
  }
3792
3896
  function writeAriaSampleFile(projectName, identities) {
3793
3897
  try {
@@ -3817,15 +3921,14 @@ function loadAriaSampleSet(projectName) {
3817
3921
  cachedSets.set(projectName, set);
3818
3922
  return set;
3819
3923
  }
3820
- function relativeTestFile(file) {
3821
- return path10.relative(process.cwd(), file).split(path10.sep).join("/");
3822
- }
3823
3924
  function isDueForAriaSample(testInfo) {
3824
3925
  const projectName = process.env.PIWI_PROJECT_NAME;
3825
3926
  if (!projectName) return false;
3826
3927
  const set = loadAriaSampleSet(projectName);
3827
3928
  if (!set || set.size === 0) return false;
3828
- return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
3929
+ return set.has(
3930
+ ariaSampleIdentity(relativeTestPath(testInfo.file, configDirFromConfig(testInfo.config)), testInfo.title)
3931
+ );
3829
3932
  }
3830
3933
 
3831
3934
  // src/internal/support/base-url-check.ts
@@ -3962,7 +4065,7 @@ async function checkBaseUrls(config, options = {}) {
3962
4065
 
3963
4066
  // src/internal/manifest/collect.ts
3964
4067
  var fs12 = __toESM(require("fs"));
3965
- var path11 = __toESM(require("path"));
4068
+ var path12 = __toESM(require("path"));
3966
4069
 
3967
4070
  // src/internal/transport/backend-request.ts
3968
4071
  var http2 = __toESM(require("http"));
@@ -4028,7 +4131,7 @@ function hasInstrumentationHeader(headers) {
4028
4131
  return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
4029
4132
  }
4030
4133
  function readCommittedManifest(configDir) {
4031
- const file = path11.join(configDir, COMMITTED_MANIFEST_FILE);
4134
+ const file = path12.join(configDir, COMMITTED_MANIFEST_FILE);
4032
4135
  try {
4033
4136
  if (!fs12.existsSync(file)) return null;
4034
4137
  return parseManifestJson(fs12.readFileSync(file, "utf8"));
@@ -4036,11 +4139,6 @@ function readCommittedManifest(configDir) {
4036
4139
  return null;
4037
4140
  }
4038
4141
  }
4039
- function configDirFromConfig(config) {
4040
- if (config?.configFile) return path11.dirname(config.configFile);
4041
- if (config?.rootDir) return config.rootDir;
4042
- return process.cwd();
4043
- }
4044
4142
  var INSTRUMENTATION_FETCH_TIMEOUT_MS = 5e3;
4045
4143
  var MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
4046
4144
  function backendManifestFetch(insecure) {
@@ -4062,8 +4160,8 @@ function backendManifestFetch(insecure) {
4062
4160
  function instrumentationManifestUrls(baseUrl) {
4063
4161
  const base = new URL(baseUrl);
4064
4162
  const root = new URL(INSTRUMENTATION_MANIFEST_PATH, base).toString();
4065
- const path34 = base.pathname.replace(/\/+$/, "");
4066
- return path34 ? [new URL(`${path34}${INSTRUMENTATION_MANIFEST_PATH}`, base).toString(), root] : [root];
4163
+ const path32 = base.pathname.replace(/\/+$/, "");
4164
+ return path32 ? [new URL(`${path32}${INSTRUMENTATION_MANIFEST_PATH}`, base).toString(), root] : [root];
4067
4165
  }
4068
4166
  async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
4069
4167
  try {
@@ -4201,13 +4299,13 @@ function normalizeFunctionRanges(ranges) {
4201
4299
  }
4202
4300
  function normalizeCoveragePath(raw) {
4203
4301
  if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
4204
- const path34 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4205
- if (!path34 || path34.startsWith("../") || path34.startsWith("/") || /^[A-Za-z]:\//.test(path34)) return null;
4206
- if (/(^|\/)node_modules\//.test(path34)) return null;
4207
- return path34;
4302
+ const path32 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4303
+ if (!path32 || path32.startsWith("../") || path32.startsWith("/") || /^[A-Za-z]:\//.test(path32)) return null;
4304
+ if (/(^|\/)node_modules\//.test(path32)) return null;
4305
+ return path32;
4208
4306
  }
4209
- function fileEntry(path34, runtime, blob, functions = [], lines = []) {
4210
- const file = { path: path34, runtime };
4307
+ function fileEntry(path32, runtime, blob, functions = [], lines = []) {
4308
+ const file = { path: path32, runtime };
4211
4309
  if (blob && (functions.length > 0 || lines.length > 0)) file.blob = blob;
4212
4310
  if (functions.length > 0) file.functions = functions;
4213
4311
  if (lines.length > 0) file.lines = lines;
@@ -4226,11 +4324,11 @@ function sanitizeCoverage(raw) {
4226
4324
  const files = [];
4227
4325
  for (const item of raw.files) {
4228
4326
  if (!isRecord(item) || !isRuntime(item.runtime)) continue;
4229
- const path34 = normalizeCoveragePath(item.path);
4230
- if (path34 === null) continue;
4327
+ const path32 = normalizeCoveragePath(item.path);
4328
+ if (path32 === null) continue;
4231
4329
  const blob = typeof item.blob === "string" && BLOB_ID.test(item.blob) ? item.blob : void 0;
4232
4330
  files.push(
4233
- fileEntry(path34, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
4331
+ fileEntry(path32, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
4234
4332
  );
4235
4333
  }
4236
4334
  const merged = mergeCoverage(captureOf(files, raw.incomplete === true));
@@ -4294,9 +4392,9 @@ function reduceCoverage(capture, granularity) {
4294
4392
  }
4295
4393
 
4296
4394
  // src/internal/collect/source-checksums.ts
4297
- var crypto3 = __toESM(require("crypto"));
4395
+ var crypto4 = __toESM(require("crypto"));
4298
4396
  var fs13 = __toESM(require("fs"));
4299
- var path12 = __toESM(require("path"));
4397
+ var path13 = __toESM(require("path"));
4300
4398
  var CHECKSUM = /^(sha1|sha256):[0-9a-f]{40,64}$/;
4301
4399
  function checksumsOf(raw) {
4302
4400
  if (!Array.isArray(raw)) return [];
@@ -4312,7 +4410,7 @@ function contentChecksums(content) {
4312
4410
  const out = /* @__PURE__ */ new Set();
4313
4411
  for (const form of lineEndingForms(content)) {
4314
4412
  for (const algorithm of ["sha1", "sha256"]) {
4315
- out.add(`${algorithm}:${crypto3.createHash(algorithm).update(form).digest("hex")}`);
4413
+ out.add(`${algorithm}:${crypto4.createHash(algorithm).update(form).digest("hex")}`);
4316
4414
  }
4317
4415
  }
4318
4416
  return out;
@@ -4361,9 +4459,9 @@ var SourceChecksums = class {
4361
4459
  }
4362
4460
  async read(relativePath2) {
4363
4461
  try {
4364
- const root = path12.resolve(this.repoRoot());
4365
- const file = path12.resolve(root, relativePath2);
4366
- if (!file.startsWith(root + path12.sep)) return null;
4462
+ const root = path13.resolve(this.repoRoot());
4463
+ const file = path13.resolve(root, relativePath2);
4464
+ if (!file.startsWith(root + path13.sep)) return null;
4367
4465
  return contentChecksums(await fs13.promises.readFile(file));
4368
4466
  } catch {
4369
4467
  return null;
@@ -4451,6 +4549,30 @@ function runFilesOf(answer) {
4451
4549
  }
4452
4550
  return files;
4453
4551
  }
4552
+ function instanceOf(answer) {
4553
+ const instance = isRecord2(answer) ? answer.instance : null;
4554
+ if (!isRecord2(instance) || typeof instance.id !== "string" || !REQUEST_ID.test(instance.id)) return null;
4555
+ const startedAt = typeof instance.startedAt === "string" && !Number.isNaN(Date.parse(instance.startedAt)) ? new Date(instance.startedAt).toISOString() : null;
4556
+ return { id: instance.id, startedAt };
4557
+ }
4558
+ function processKeyOf(id) {
4559
+ const dot = id.lastIndexOf(".");
4560
+ return dot > 0 ? id.slice(0, dot) : null;
4561
+ }
4562
+ function withTestCoverage(files, tests) {
4563
+ if (!tests || tests.files.length === 0) return files;
4564
+ const byKey = new Map(files.map((file) => [`${file.runtime}\0${file.path}`, { ...file }]));
4565
+ for (const ran of tests.files) {
4566
+ const key = `${ran.runtime}\0${ran.path}`;
4567
+ const file = byKey.get(key) ?? { path: ran.path, runtime: ran.runtime };
4568
+ const functions = normalizeFunctionRanges([...file.functions ?? [], ...ran.functions ?? []]);
4569
+ const lines = normalizeLineRanges([...file.lines ?? [], ...ran.lines ?? []]);
4570
+ if (functions.length > 0) file.functions = functions;
4571
+ if (lines.length > 0) file.lines = lines;
4572
+ byKey.set(key, file);
4573
+ }
4574
+ return [...byKey.values()];
4575
+ }
4454
4576
  function isCollectAnswer(answer) {
4455
4577
  return isRecord2(answer) && answer.v === 1 && Array.isArray(answer.groups);
4456
4578
  }
@@ -4502,11 +4624,16 @@ var ServerCoverageCollector = class {
4502
4624
  }
4503
4625
  /**
4504
4626
  * The run-level coverage of an origin, with each server file's executable
4505
- * lines at `lines` granularity; null when the backend did not answer with it.
4627
+ * lines at `lines` granularity, and the lines and functions the tests'
4628
+ * coverage named added to it. The gap says why part of it is missing: the
4629
+ * backend restarted during the run (its answer names another process than
4630
+ * the run's first request ids), or it did not answer with run-level coverage.
4506
4631
  */
4507
4632
  async fetchRun(origin) {
4508
4633
  const state = this.stateOf(origin);
4509
4634
  const query = `?granularity=${encodeURIComponent(this.options.granularity)}`;
4635
+ let answer = null;
4636
+ let failure = null;
4510
4637
  for (const base of this.pathBases(origin)) {
4511
4638
  const outcome = await this.call(
4512
4639
  `${origin}${base}${RUN_PATH}${query}`,
@@ -4514,21 +4641,47 @@ var ServerCoverageCollector = class {
4514
4641
  this.limits.runTimeoutMs
4515
4642
  );
4516
4643
  if (outcome.kind === "failed") {
4517
- this.reportFailure(state, `Could not fetch the run's server coverage from ${origin}: ${outcome.reason}.`);
4518
- return null;
4644
+ failure = outcome.reason;
4645
+ break;
4519
4646
  }
4520
4647
  const answered = outcome.ok ? runFilesOf(outcome.json) : null;
4521
4648
  if (answered) {
4522
4649
  state.base = base;
4523
- const files = await this.verifyChecksums(
4524
- answered,
4525
- fileChecksums(isRecord2(outcome.json) ? outcome.json.files : null)
4526
- );
4527
- return this.options.blobs ? this.options.blobs.withBlobs(files.files) : files.files;
4650
+ answer = { files: answered, json: outcome.json };
4651
+ break;
4528
4652
  }
4529
4653
  }
4530
- this.reportFailure(state, `Could not fetch the run's server coverage: ${this.noEndpoint(origin, RUN_PATH)}.`);
4531
- return null;
4654
+ const gap = answer ? this.restartGap(origin, state, instanceOf(answer.json)) : { origin, reason: "unreachable" };
4655
+ if (!answer) failure ??= this.noEndpoint(origin, RUN_PATH);
4656
+ if (gap && !this.closer.signal.aborted) this.logger.warn(this.gapMessage(gap, failure));
4657
+ const checksums = fileChecksums(answer?.json.files ?? null, new Map(state.testChecksums));
4658
+ const files = await this.verifyChecksums(withTestCoverage(answer?.files ?? [], state.testCoverage), checksums);
4659
+ let out = files.files;
4660
+ try {
4661
+ if (this.options.blobs) out = await this.options.blobs.withBlobs(out);
4662
+ } catch {
4663
+ }
4664
+ return { files: out, gap };
4665
+ }
4666
+ /**
4667
+ * The gap of an origin whose run-level coverage `instance` answered: a
4668
+ * restart when it, or a collection answer, names another process than the
4669
+ * run's first request ids; null when the backend names no process.
4670
+ */
4671
+ restartGap(origin, state, instance) {
4672
+ if (!instance) return null;
4673
+ const keys = [...state.processKeys];
4674
+ const others = [...state.instances.values()].filter((known) => known.id !== instance.id);
4675
+ if (keys.every((key) => key === instance.id) && others.length === 0) return null;
4676
+ const at = keys[0] === instance.id ? others[others.length - 1]?.startedAt ?? null : instance.startedAt;
4677
+ return { origin, reason: "restarted", ...at ? { at } : {} };
4678
+ }
4679
+ gapMessage(gap, failure) {
4680
+ if (gap.reason === "unreachable") {
4681
+ return `Could not fetch the run's server coverage from ${gap.origin} (${failure}): the run's server coverage is marked incomplete, and keeps the lines the tests' coverage named.`;
4682
+ }
4683
+ const when = gap.at ? ` at about ${gap.at}` : "";
4684
+ return `The backend at ${gap.origin} restarted during the run${when}: the run's server coverage is marked incomplete. It keeps the lines the tests' coverage named; what the earlier process ran outside a request is lost.`;
4532
4685
  }
4533
4686
  /** Abort every call in flight and fail every later one; the tests still waiting resolve incomplete. */
4534
4687
  close() {
@@ -4550,7 +4703,11 @@ var ServerCoverageCollector = class {
4550
4703
  timer: null,
4551
4704
  base: null,
4552
4705
  limit: createLimiter(this.limits.concurrency),
4553
- warned: false
4706
+ warned: false,
4707
+ processKeys: /* @__PURE__ */ new Set(),
4708
+ instances: /* @__PURE__ */ new Map(),
4709
+ testCoverage: null,
4710
+ testChecksums: /* @__PURE__ */ new Map()
4554
4711
  };
4555
4712
  this.originStates.set(origin, state);
4556
4713
  }
@@ -4620,6 +4777,13 @@ var ServerCoverageCollector = class {
4620
4777
  }
4621
4778
  /** Send one call and hand each group's answer to its test. */
4622
4779
  async send(origin, groups) {
4780
+ const state = this.stateOf(origin);
4781
+ for (const group of groups) {
4782
+ for (const id of group.ids) {
4783
+ const key = processKeyOf(id);
4784
+ if (key !== null) state.processKeys.add(key);
4785
+ }
4786
+ }
4623
4787
  let answer = null;
4624
4788
  try {
4625
4789
  const body = JSON.stringify({
@@ -4630,6 +4794,8 @@ var ServerCoverageCollector = class {
4630
4794
  } catch (error) {
4631
4795
  this.logger.debug(`Server coverage collection from ${origin} failed: ${errorMessage(error)}`);
4632
4796
  }
4797
+ const instance = instanceOf(answer);
4798
+ if (instance && !state.instances.has(instance.id)) state.instances.set(instance.id, instance);
4633
4799
  const answered = /* @__PURE__ */ new Map();
4634
4800
  for (const group of answer?.groups ?? []) {
4635
4801
  if (isRecord2(group) && typeof group.key === "string") answered.set(group.key, group);
@@ -4641,9 +4807,11 @@ var ServerCoverageCollector = class {
4641
4807
  } else {
4642
4808
  if (Array.isArray(result.missing) && result.missing.length > 0) group.entry.incomplete = true;
4643
4809
  fileChecksums(result.files, group.entry.checksums);
4810
+ fileChecksums(result.files, state.testChecksums);
4644
4811
  const capture = sanitizeCoverage({ v: 1, files: result.files });
4645
4812
  if (capture) {
4646
4813
  group.entry.captures.push(capture);
4814
+ state.testCoverage = mergeCoverage(state.testCoverage, capture);
4647
4815
  if (capture.incomplete) group.entry.incomplete = true;
4648
4816
  }
4649
4817
  }
@@ -4771,14 +4939,14 @@ var ServerCoverageCollector = class {
4771
4939
  // src/public/global-setup.ts
4772
4940
  function createGlobalSetup(options, userSetup) {
4773
4941
  return async function globalSetupFn(config) {
4774
- const piwiReporterPath = path13.resolve(__dirname, "./index.js");
4942
+ const piwiReporterPath = path14.resolve(__dirname, "./index.js");
4775
4943
  let inlineReporterOptions = {};
4776
4944
  if (Array.isArray(config?.reporter)) {
4777
4945
  for (const r of config.reporter) {
4778
4946
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
4779
4947
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
4780
4948
  try {
4781
- return path13.resolve(require.resolve(r[0])) === piwiReporterPath;
4949
+ return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
4782
4950
  } catch {
4783
4951
  return false;
4784
4952
  }
@@ -4814,7 +4982,7 @@ function createGlobalSetup(options, userSetup) {
4814
4982
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
4815
4983
  if (r[0].toLowerCase().includes("piwi")) return true;
4816
4984
  try {
4817
- return path13.resolve(require.resolve(r[0])) === piwiReporterPath;
4985
+ return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
4818
4986
  } catch {
4819
4987
  return false;
4820
4988
  }
@@ -4898,12 +5066,11 @@ function createGlobalSetup(options, userSetup) {
4898
5066
  }
4899
5067
 
4900
5068
  // src/public/config-wrapper.ts
4901
- var path16 = __toESM(require("path"));
5069
+ var path15 = __toESM(require("path"));
4902
5070
  var fs17 = __toESM(require("fs"));
4903
5071
 
4904
5072
  // src/internal/probe/mode.ts
4905
5073
  var fs15 = __toESM(require("fs"));
4906
- var path14 = __toESM(require("path"));
4907
5074
 
4908
5075
  // src/internal/probe/plan.ts
4909
5076
  function probeRunMetadata(base = {}) {
@@ -4915,8 +5082,8 @@ function parseRouteKey(routeKey2) {
4915
5082
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
4916
5083
  }
4917
5084
  function patternToRegex(pattern) {
4918
- const path34 = pattern.split("?")[0] ?? pattern;
4919
- const escaped = path34.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
5085
+ const path32 = pattern.split("?")[0] ?? pattern;
5086
+ const escaped = path32.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
4920
5087
  return new RegExp(`^${escaped}$`);
4921
5088
  }
4922
5089
  function suitePathEqual(a, b) {
@@ -4939,13 +5106,13 @@ function matchProbeItem(plan, test) {
4939
5106
  function requestMatchesRoute(method, url, routeKey2) {
4940
5107
  const { method: rm, pattern } = parseRouteKey(routeKey2);
4941
5108
  if (rm && rm !== method.toUpperCase()) return false;
4942
- let path34;
5109
+ let path32;
4943
5110
  try {
4944
- path34 = new URL(url).pathname;
5111
+ path32 = new URL(url).pathname;
4945
5112
  } catch {
4946
- path34 = url.split("?")[0] ?? url;
5113
+ path32 = url.split("?")[0] ?? url;
4947
5114
  }
4948
- return patternToRegex(pattern).test(path34);
5115
+ return patternToRegex(pattern).test(path32);
4949
5116
  }
4950
5117
  function createProbeState() {
4951
5118
  return { navigated: false, matches: 0 };
@@ -4967,10 +5134,10 @@ function isProbeMode() {
4967
5134
  function loadPlan() {
4968
5135
  if (planCache !== void 0) return planCache;
4969
5136
  planCache = null;
4970
- const path34 = process.env[PIWI_PROBE_ENV.plan];
4971
- if (path34) {
5137
+ const path32 = process.env[PIWI_PROBE_ENV.plan];
5138
+ if (path32) {
4972
5139
  try {
4973
- planCache = JSON.parse(fs15.readFileSync(path34, "utf8"));
5140
+ planCache = JSON.parse(fs15.readFileSync(path32, "utf8"));
4974
5141
  } catch {
4975
5142
  planCache = null;
4976
5143
  }
@@ -4980,7 +5147,7 @@ function loadPlan() {
4980
5147
  function probeItemForTest(test) {
4981
5148
  const plan = loadPlan();
4982
5149
  if (!plan) return null;
4983
- const filePath2 = test.file ? path14.relative(process.cwd(), test.file).split(path14.sep).join("/") : null;
5150
+ const filePath2 = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
4984
5151
  const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
4985
5152
  return matchProbeItem(plan, { title: test.title, filePath: filePath2, suitePath });
4986
5153
  }
@@ -4994,10 +5161,10 @@ function outcomeFromStatus(status, applied) {
4994
5161
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
4995
5162
  }
4996
5163
  function recordProbeOutcome(line) {
4997
- const path34 = process.env[PIWI_PROBE_ENV.results];
4998
- if (!path34) return;
5164
+ const path32 = process.env[PIWI_PROBE_ENV.results];
5165
+ if (!path32) return;
4999
5166
  try {
5000
- fs15.appendFileSync(path34, `${JSON.stringify(line)}
5167
+ fs15.appendFileSync(path32, `${JSON.stringify(line)}
5001
5168
  `);
5002
5169
  } catch {
5003
5170
  }
@@ -5005,7 +5172,6 @@ function recordProbeOutcome(line) {
5005
5172
 
5006
5173
  // src/internal/flake/mode.ts
5007
5174
  var fs16 = __toESM(require("fs"));
5008
- var path15 = __toESM(require("path"));
5009
5175
 
5010
5176
  // ../core/src/error-signature.ts
5011
5177
  var UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
@@ -5525,12 +5691,12 @@ function flakeRunMetadata(stamp, base = {}) {
5525
5691
  [RUN_ORIGIN_METADATA_KEY]: ref === void 0 ? { kind: "flake-lab" } : { kind: "flake-lab", ref }
5526
5692
  };
5527
5693
  }
5528
- function relativeTestFile2(file) {
5529
- return file ? path15.relative(process.cwd(), file).split(path15.sep).join("/") : null;
5694
+ function relativeTestFile(file, base = process.cwd()) {
5695
+ return file ? relativeTestPath(file, base) : null;
5530
5696
  }
5531
5697
  function flakeRoleForTest(plan, test) {
5532
5698
  const running = {
5533
- file: relativeTestFile2(test.file),
5699
+ file: relativeTestFile(test.file, test.base),
5534
5700
  title: test.title,
5535
5701
  suite: test.titlePath ? test.titlePath.slice(1, -1) : void 0
5536
5702
  };
@@ -5646,7 +5812,7 @@ function flakeResultLine(plan, attempt) {
5646
5812
  experimentId: plan.experimentId,
5647
5813
  armId: plan.arm.id,
5648
5814
  role: attempt.role,
5649
- file: relativeTestFile2(attempt.file),
5815
+ file: relativeTestFile(attempt.file, attempt.base),
5650
5816
  title: attempt.title,
5651
5817
  project: attempt.project,
5652
5818
  browserName: attempt.browserName,
@@ -5737,9 +5903,9 @@ function injectReporter(reporter, piwiOptions) {
5737
5903
  }
5738
5904
  function resolveSetupModule() {
5739
5905
  const candidates = [
5740
- path16.join(__dirname, "global-setup-module.js"),
5741
- path16.join(__dirname, "..", "global-setup-module.js"),
5742
- path16.join(__dirname, "..", "global-setup-module.ts")
5906
+ path15.join(__dirname, "global-setup-module.js"),
5907
+ path15.join(__dirname, "..", "global-setup-module.js"),
5908
+ path15.join(__dirname, "..", "global-setup-module.ts")
5743
5909
  ];
5744
5910
  return candidates.find((candidate) => fs17.existsSync(candidate)) ?? candidates[0];
5745
5911
  }
@@ -5949,9 +6115,6 @@ function collectTestMetadata(annotations) {
5949
6115
  return parseTestMetadata(annotations);
5950
6116
  }
5951
6117
 
5952
- // src/internal/collect/error-text.ts
5953
- var path17 = __toESM(require("path"));
5954
-
5955
6118
  // ../core/src/error-text.ts
5956
6119
  function joinErrorMessages(errors) {
5957
6120
  if (!errors || errors.length === 0) return null;
@@ -5968,24 +6131,26 @@ function joinErrorMessages(errors) {
5968
6131
  return parts.join("\n---\n") || null;
5969
6132
  }
5970
6133
  function appendErrorLocation(text, location) {
5971
- if (!location?.file || /\n\s+at\s/.test(text)) return text;
6134
+ if (!location?.file || /\n[ \t]+at\s/.test(text)) return text;
5972
6135
  return `${text}
5973
6136
  at ${location.file}:${location.line}:${location.column}`;
5974
6137
  }
5975
6138
 
5976
6139
  // src/internal/collect/error-text.ts
5977
- function buildErrorText(result) {
6140
+ function buildErrorText(result, base = process.cwd()) {
5978
6141
  const text = joinErrorMessages(result.errors);
5979
6142
  if (!text) return null;
5980
6143
  const loc = result.error?.location;
5981
6144
  if (!loc?.file) return text;
5982
- const rel = path17.relative(process.cwd(), loc.file).split(path17.sep).join("/");
5983
- return appendErrorLocation(text, { file: rel, line: loc.line, column: loc.column });
6145
+ return appendErrorLocation(text, { file: relativeTestPath(loc.file, base), line: loc.line, column: loc.column });
5984
6146
  }
5985
6147
 
6148
+ // src/internal/submit/run-submitter.ts
6149
+ var import_node_crypto2 = require("crypto");
6150
+
5986
6151
  // src/internal/support/ci-output.ts
5987
6152
  var fs18 = __toESM(require("fs"));
5988
- var path18 = __toESM(require("path"));
6153
+ var path16 = __toESM(require("path"));
5989
6154
  var SUMMARY_MAX_FAILURES = 20;
5990
6155
  function emitRunOutputs(output, logger, outputFile, env = process.env) {
5991
6156
  logger.info(`View run: ${output.runUrl}`);
@@ -5995,7 +6160,7 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
5995
6160
  }
5996
6161
  function writeOutputFile(file, output, logger) {
5997
6162
  try {
5998
- const dir = path18.dirname(file);
6163
+ const dir = path16.dirname(file);
5999
6164
  if (dir && dir !== ".") fs18.mkdirSync(dir, { recursive: true });
6000
6165
  fs18.writeFileSync(
6001
6166
  file,
@@ -6107,8 +6272,8 @@ function ciBuildUrlFromMetadata(metadata) {
6107
6272
 
6108
6273
  // src/internal/submit/run-submitter.ts
6109
6274
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
6110
- function markSubmitFallback(run, path34, reason) {
6111
- run.metadata.ingestHealth = { submitFallback: { path: path34, reason } };
6275
+ function markSubmitFallback(run, path32, reason) {
6276
+ run.metadata.ingestHealth = { submitFallback: { path: path32, reason } };
6112
6277
  }
6113
6278
  var RunSubmitter = class {
6114
6279
  /**
@@ -6126,6 +6291,8 @@ var RunSubmitter = class {
6126
6291
  this.streamManager = streamManager;
6127
6292
  this.logger = logger;
6128
6293
  this.failureLinks = failureLinks;
6294
+ /** The key every batch delivery of this run carries, so the dashboard stores the run once. */
6295
+ this.deliveryKey = (0, import_node_crypto2.randomUUID)();
6129
6296
  }
6130
6297
  /**
6131
6298
  * Run the fallback ladder for a completed test run, within the
@@ -6181,8 +6348,9 @@ var RunSubmitter = class {
6181
6348
  auth = sm?.auth ?? await this.httpClient.resolveAuth(run.options);
6182
6349
  } catch (error) {
6183
6350
  this.logger.error(`Authentication failed: ${errorMessage(error)}`);
6351
+ this.logRecoverySaved();
6184
6352
  this.saveRecovery(this.buildRunPayload(run, overallStatus, duration));
6185
- throw error;
6353
+ return null;
6186
6354
  }
6187
6355
  if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
6188
6356
  let outcome = { done: false, output: null };
@@ -6256,7 +6424,8 @@ var RunSubmitter = class {
6256
6424
  shardTotal: run.shardInfo?.total,
6257
6425
  isFullRun: run.isFullRun,
6258
6426
  filterDetails: run.filterDetails,
6259
- resourceReport: run.resourceReport ?? null
6427
+ resourceReport: run.resourceReport ?? null,
6428
+ deliveryKey: this.deliveryKey
6260
6429
  };
6261
6430
  }
6262
6431
  async tryFinishStreaming(run, overallStatus, duration, auth) {
@@ -6322,11 +6491,7 @@ var RunSubmitter = class {
6322
6491
  this.dropLocalCopies();
6323
6492
  return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
6324
6493
  } catch (error) {
6325
- if (error instanceof HttpError && error.status === 401 && !auth) {
6326
- this.logAuthRequired(run.options.serverUrl);
6327
- this.saveRecovery(payload);
6328
- throw error;
6329
- }
6494
+ if (error instanceof HttpError && error.status === 401 && !auth) return this.stopForMissingAuth(run, payload);
6330
6495
  this.logger.warn(`Failed to upload with files: ${errorMessage(error)}`);
6331
6496
  this.logger.info("Falling back to JSON upload...");
6332
6497
  return { done: false, output: null };
@@ -6339,22 +6504,20 @@ var RunSubmitter = class {
6339
6504
  this.dropLocalCopies();
6340
6505
  return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
6341
6506
  } catch (error) {
6342
- if (error instanceof HttpError && error.status === 401 && !auth) {
6343
- this.logAuthRequired(run.options.serverUrl);
6344
- this.saveRecovery(payload);
6345
- throw error;
6346
- }
6507
+ if (error instanceof HttpError && error.status === 401 && !auth) return this.stopForMissingAuth(run, payload);
6347
6508
  this.logger.error(`All upload methods failed: ${errorMessage(error)}`);
6348
- this.logger.info(
6349
- `Saved a local recovery copy \u2014 it will be uploaded automatically on your next test run. If this keeps happening, check that serverUrl (${run.options.serverUrl ?? "not set"}) is correct and reachable.`
6509
+ this.logRecoverySaved(
6510
+ `If this keeps happening, check that serverUrl (${run.options.serverUrl ?? "not set"}) is correct and reachable.`
6350
6511
  );
6351
6512
  this.saveRecovery(payload);
6352
6513
  return { done: true, output: null };
6353
6514
  }
6354
6515
  }
6355
- /** The run reached the server: drop the recovery copy and the run's buffered live events. */
6516
+ /**
6517
+ * The run reached the server: drop its buffered live events. A recovery file
6518
+ * on disk is an earlier run's, deleted only once its own replay succeeds.
6519
+ */
6356
6520
  dropLocalCopies() {
6357
- this.recovery.clear();
6358
6521
  this.streamManager?.discardBuffered();
6359
6522
  }
6360
6523
  /**
@@ -6364,22 +6527,35 @@ var RunSubmitter = class {
6364
6527
  saveRecovery(payload) {
6365
6528
  this.recovery.save(serializeRun(payload, { includeTestCases: true }));
6366
6529
  }
6367
- /** Log one actionable line explaining how to fix a 401 caused by a missing credential. */
6368
- logAuthRequired(serverUrl) {
6530
+ /**
6531
+ * The dashboard asked for a credential the reporter has none of: no other
6532
+ * rung can succeed, so the ladder stops with the run saved for the next one.
6533
+ * The test run's own result stays what Playwright made it.
6534
+ */
6535
+ stopForMissingAuth(run, payload) {
6369
6536
  this.logger.error(
6370
- `Authentication is required by ${serverUrl ?? "the dashboard"} but no credentials were configured. Create an API key (Settings \u2192 Users on the dashboard) and set the reporter's \`apiKey\` option or the PIWI_API_KEY environment variable.`
6537
+ `Authentication is required by ${run.options.serverUrl ?? "the dashboard"} but no credentials were configured. Create an API key (Settings \u2192 Users on the dashboard) and set the reporter's \`apiKey\` option or the PIWI_API_KEY environment variable.`
6538
+ );
6539
+ this.logRecoverySaved();
6540
+ this.saveRecovery(payload);
6541
+ return { done: true, output: null };
6542
+ }
6543
+ /** Say where the run went when the dashboard did not take it, with an optional hint. */
6544
+ logRecoverySaved(hint) {
6545
+ this.logger.info(
6546
+ `Saved a local recovery copy \u2014 it will be uploaded automatically on your next test run.${hint ? ` ${hint}` : ""}`
6371
6547
  );
6372
6548
  }
6373
6549
  };
6374
6550
 
6375
6551
  // src/internal/capture/resource-ledger.ts
6376
6552
  var fs20 = __toESM(require("fs"));
6377
- var path21 = __toESM(require("path"));
6553
+ var path19 = __toESM(require("path"));
6378
6554
  var import_node_module2 = require("module");
6379
6555
 
6380
6556
  // src/internal/capture/quiet-capture.ts
6381
6557
  var import_node_module = require("module");
6382
- var path19 = __toESM(require("path"));
6558
+ var path17 = __toESM(require("path"));
6383
6559
  var BOXED_PREFIX_EXPORTS = [
6384
6560
  [
6385
6561
  "playwright-core/lib/coreBundle",
@@ -6416,7 +6592,7 @@ function resolveBoxing() {
6416
6592
  if (typeof set !== "function") continue;
6417
6593
  return {
6418
6594
  set,
6419
- prefixes: [path19.dirname(playwrightPackageJson), path19.join(root, "dist") + path19.sep]
6595
+ prefixes: [path17.dirname(playwrightPackageJson), path17.join(root, "dist") + path17.sep]
6420
6596
  };
6421
6597
  }
6422
6598
  } catch {
@@ -6449,7 +6625,7 @@ var import_node_perf_hooks = require("perf_hooks");
6449
6625
 
6450
6626
  // src/internal/support/system-readers.ts
6451
6627
  var fs19 = __toESM(require("fs"));
6452
- var path20 = __toESM(require("path"));
6628
+ var path18 = __toESM(require("path"));
6453
6629
  var CLK_TCK = 100;
6454
6630
  function readText(file) {
6455
6631
  try {
@@ -6587,7 +6763,7 @@ var ProcFs = class {
6587
6763
  }
6588
6764
  /** Whether this looks like a Linux `/proc` at all. */
6589
6765
  available() {
6590
- return fs19.existsSync(path20.join(this.root, "stat"));
6766
+ return fs19.existsSync(path18.join(this.root, "stat"));
6591
6767
  }
6592
6768
  /** Every process, keyed by pid. */
6593
6769
  table() {
@@ -6600,32 +6776,32 @@ var ProcFs = class {
6600
6776
  }
6601
6777
  for (const name of names) {
6602
6778
  if (!/^\d+$/.test(name)) continue;
6603
- const text = readText(path20.join(this.root, name, "stat"));
6779
+ const text = readText(path18.join(this.root, name, "stat"));
6604
6780
  const stat = text ? parseProcStat(Number(name), text) : null;
6605
6781
  if (stat) out.set(stat.pid, stat);
6606
6782
  }
6607
6783
  return out;
6608
6784
  }
6609
6785
  stat(pid) {
6610
- const text = readText(path20.join(this.root, String(pid), "stat"));
6786
+ const text = readText(path18.join(this.root, String(pid), "stat"));
6611
6787
  return text ? parseProcStat(pid, text) : null;
6612
6788
  }
6613
6789
  runWaitMs(pid) {
6614
- const text = readText(path20.join(this.root, String(pid), "schedstat"));
6790
+ const text = readText(path18.join(this.root, String(pid), "schedstat"));
6615
6791
  return text ? parseSchedstat(text) : null;
6616
6792
  }
6617
6793
  cmdline(pid) {
6618
- return (readText(path20.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
6794
+ return (readText(path18.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
6619
6795
  }
6620
6796
  /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
6621
6797
  statusKb(pid, key) {
6622
- const text = readText(path20.join(this.root, String(pid), "status"));
6798
+ const text = readText(path18.join(this.root, String(pid), "status"));
6623
6799
  return text ? parseKbField(text, key) : null;
6624
6800
  }
6625
6801
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
6626
6802
  async pssKb(pid) {
6627
6803
  try {
6628
- return parseKbField(await fs19.promises.readFile(path20.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
6804
+ return parseKbField(await fs19.promises.readFile(path18.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
6629
6805
  } catch {
6630
6806
  return null;
6631
6807
  }
@@ -6633,29 +6809,29 @@ var ProcFs = class {
6633
6809
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
6634
6810
  resetPeakRss(pid) {
6635
6811
  try {
6636
- fs19.writeFileSync(path20.join(this.root, String(pid), "clear_refs"), "5");
6812
+ fs19.writeFileSync(path18.join(this.root, String(pid), "clear_refs"), "5");
6637
6813
  return true;
6638
6814
  } catch {
6639
6815
  return false;
6640
6816
  }
6641
6817
  }
6642
6818
  cpuTicks() {
6643
- const text = readText(path20.join(this.root, "stat"));
6819
+ const text = readText(path18.join(this.root, "stat"));
6644
6820
  return text ? parseCpuTicks(text) : null;
6645
6821
  }
6646
6822
  /** Total stall time of a resource's pressure file, in µs. */
6647
6823
  pressureUs(resource) {
6648
- const text = readText(path20.join(this.root, "pressure", resource));
6824
+ const text = readText(path18.join(this.root, "pressure", resource));
6649
6825
  return text ? parsePsiTotal(text) : null;
6650
6826
  }
6651
6827
  memAvailableKb() {
6652
- const text = readText(path20.join(this.root, "meminfo"));
6828
+ const text = readText(path18.join(this.root, "meminfo"));
6653
6829
  return text ? parseKbField(text, "MemAvailable") : null;
6654
6830
  }
6655
6831
  /** Open file descriptors of a process. */
6656
6832
  fdCount(pid) {
6657
6833
  try {
6658
- return fs19.readdirSync(path20.join(this.root, String(pid), "fd")).length;
6834
+ return fs19.readdirSync(path18.join(this.root, String(pid), "fd")).length;
6659
6835
  } catch {
6660
6836
  return null;
6661
6837
  }
@@ -6676,7 +6852,7 @@ var CgroupFs = class {
6676
6852
  }
6677
6853
  /** The directory of a controller (v1) or of the unified hierarchy (v2), when it exists. */
6678
6854
  dirs() {
6679
- const text = readText(path20.join(this.procRoot, String(this.pid), "cgroup"));
6855
+ const text = readText(path18.join(this.procRoot, String(this.pid), "cgroup"));
6680
6856
  if (!text) return null;
6681
6857
  const v1 = /* @__PURE__ */ new Map();
6682
6858
  let v2 = null;
@@ -6687,21 +6863,21 @@ var CgroupFs = class {
6687
6863
  else for (const controller of match[1].split(",")) v1.set(controller, match[2]);
6688
6864
  }
6689
6865
  const pick = (mount, sub) => {
6690
- const nested = path20.join(mount, sub);
6866
+ const nested = path18.join(mount, sub);
6691
6867
  if (fs19.existsSync(nested)) return nested;
6692
6868
  return fs19.existsSync(mount) ? mount : null;
6693
6869
  };
6694
6870
  if (v1.has("memory") || v1.has("cpu")) {
6695
6871
  const memoryPath = v1.get("memory");
6696
6872
  const cpuPath = v1.get("cpu");
6697
- const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path20.join(this.cgroupRoot, name)).find((dir) => fs19.existsSync(dir));
6873
+ const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path18.join(this.cgroupRoot, name)).find((dir) => fs19.existsSync(dir));
6698
6874
  return {
6699
6875
  version: 1,
6700
- memory: memoryPath !== void 0 ? pick(path20.join(this.cgroupRoot, "memory"), memoryPath) : null,
6876
+ memory: memoryPath !== void 0 ? pick(path18.join(this.cgroupRoot, "memory"), memoryPath) : null,
6701
6877
  cpu: cpuPath !== void 0 && cpuMount ? pick(cpuMount, cpuPath) : null
6702
6878
  };
6703
6879
  }
6704
- if (v2 !== null && fs19.existsSync(path20.join(this.cgroupRoot, "cgroup.controllers"))) {
6880
+ if (v2 !== null && fs19.existsSync(path18.join(this.cgroupRoot, "cgroup.controllers"))) {
6705
6881
  const dir = pick(this.cgroupRoot, v2);
6706
6882
  return { version: 2, memory: dir, cpu: dir };
6707
6883
  }
@@ -6710,7 +6886,7 @@ var CgroupFs = class {
6710
6886
  read() {
6711
6887
  const dirs = this.dirs();
6712
6888
  if (!dirs) return null;
6713
- const file = (dir, name) => dir ? readText(path20.join(dir, name)) : null;
6889
+ const file = (dir, name) => dir ? readText(path18.join(dir, name)) : null;
6714
6890
  if (dirs.version === 2) {
6715
6891
  const max = file(dirs.memory, "memory.max");
6716
6892
  const cpuMax = file(dirs.cpu, "cpu.max")?.trim().split(/\s+/);
@@ -6896,10 +7072,10 @@ var PAGE_READ_TIMEOUT_MS = 1e3;
6896
7072
  var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
6897
7073
  var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
6898
7074
  function relativePath(file) {
6899
- return path21.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path21.sep).join("/");
7075
+ return path19.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path19.sep).join("/");
6900
7076
  }
6901
7077
  function isLibraryFile(file) {
6902
- return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path21.sep}node_modules${path21.sep}`) || file.includes("/node_modules/") || file === __filename;
7078
+ return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path19.sep}node_modules${path19.sep}`) || file.includes("/node_modules/") || file === __filename;
6903
7079
  }
6904
7080
  function userSite(stack) {
6905
7081
  for (const line of (stack ?? "").split("\n").slice(1)) {
@@ -6947,7 +7123,7 @@ function playwrightTestInfoSource() {
6947
7123
  try {
6948
7124
  const packageJson = resolvePlaywrightPackageJson();
6949
7125
  if (!packageJson) return () => null;
6950
- const globals = (0, import_node_module2.createRequire)(packageJson)(path21.join(path21.dirname(packageJson), "lib", "globals.js"));
7126
+ const globals = (0, import_node_module2.createRequire)(packageJson)(path19.join(path19.dirname(packageJson), "lib", "globals.js"));
6951
7127
  const read = globals.currentTestInfo;
6952
7128
  if (typeof read !== "function") return () => null;
6953
7129
  return () => {
@@ -7436,7 +7612,7 @@ async function recordResourceCensus(testInfo) {
7436
7612
  }
7437
7613
 
7438
7614
  // src/internal/collect/resource-verdicts.ts
7439
- var path22 = __toESM(require("path"));
7615
+ var path20 = __toESM(require("path"));
7440
7616
  var OPEN_STEP_TITLES = {
7441
7617
  "Create context": "context",
7442
7618
  "Launch persistent context": "context",
@@ -7468,7 +7644,7 @@ function userFixturesOf(steps) {
7468
7644
  const walk = (list) => {
7469
7645
  for (const step of list) {
7470
7646
  const file = step.location?.file;
7471
- if (step.category === "fixture" && file && !file.includes(`${path22.sep}node_modules${path22.sep}`)) {
7647
+ if (step.category === "fixture" && file && !file.includes(`${path20.sep}node_modules${path20.sep}`)) {
7472
7648
  const name = step.title.replace(/^(?:Fixture\s+|fixture:\s*)/, "").replace(/^"(.*)"$/, "$1");
7473
7649
  if (!name.startsWith("piwi")) out.push(`${name} (${relativeFile(file)}:${step.location.line})`);
7474
7650
  }
@@ -7501,7 +7677,7 @@ function tallyLifecycleSteps(file, steps) {
7501
7677
  return tally;
7502
7678
  }
7503
7679
  function relativeFile(file) {
7504
- return path22.relative(process.cwd(), file).split(path22.sep).join("/");
7680
+ return path20.relative(process.cwd(), file).split(path20.sep).join("/");
7505
7681
  }
7506
7682
  function buildResourceReport(input) {
7507
7683
  const findings = [];
@@ -7861,7 +8037,7 @@ function formatResourceSummary(report, leakCheck, maxLines = 10) {
7861
8037
  // src/internal/collect/process-sampler.ts
7862
8038
  var fs21 = __toESM(require("fs"));
7863
8039
  var os8 = __toESM(require("os"));
7864
- var path23 = __toESM(require("path"));
8040
+ var path21 = __toESM(require("path"));
7865
8041
  var import_node_child_process2 = require("child_process");
7866
8042
  var WALK_FILE_CAP = 1e4;
7867
8043
  var SERIES_CAP = 3600;
@@ -7926,13 +8102,13 @@ function defaultWindowsQuery() {
7926
8102
  });
7927
8103
  }
7928
8104
  function freeBytes(dir) {
7929
- let current = path23.resolve(dir);
8105
+ let current = path21.resolve(dir);
7930
8106
  for (; ; ) {
7931
8107
  try {
7932
8108
  const stats = fs21.statfsSync(current);
7933
8109
  return stats.bavail * stats.bsize;
7934
8110
  } catch {
7935
- const parent = path23.dirname(current);
8111
+ const parent = path21.dirname(current);
7936
8112
  if (parent === current) return null;
7937
8113
  current = parent;
7938
8114
  }
@@ -7948,7 +8124,7 @@ async function dirBytes(dir, budget) {
7948
8124
  }
7949
8125
  for (const entry of entries) {
7950
8126
  if (budget.files <= 0) return bytes;
7951
- const full = path23.join(dir, entry.name);
8127
+ const full = path21.join(dir, entry.name);
7952
8128
  if (entry.isDirectory()) bytes += await dirBytes(full, budget);
7953
8129
  else if (entry.isFile()) {
7954
8130
  budget.files--;
@@ -7993,7 +8169,7 @@ var RunSampler = class {
7993
8169
  this.platform = options.platform ?? process.platform;
7994
8170
  this.proc = new ProcFs(options.procRoot);
7995
8171
  this.cgroup = new CgroupFs(options.procRoot, options.cgroupRoot);
7996
- this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path23.resolve(dir)))];
8172
+ this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path21.resolve(dir)))];
7997
8173
  this.tmpDir = options.tmpDir ?? os8.tmpdir();
7998
8174
  this.intervalMs = options.intervalMs ?? 1e3;
7999
8175
  this.heavyEvery = Math.max(1, options.heavyEvery ?? 5);
@@ -8195,7 +8371,7 @@ var RunSampler = class {
8195
8371
  } catch {
8196
8372
  return;
8197
8373
  }
8198
- this.noteTree(list, (proc) => roleOf(path23.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
8374
+ this.noteTree(list, (proc) => roleOf(path21.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
8199
8375
  }
8200
8376
  async sampleWindowsTree(at) {
8201
8377
  let list;
@@ -8251,7 +8427,7 @@ var RunSampler = class {
8251
8427
  let bytes = 0;
8252
8428
  for (const dir of this.outputDirs) bytes += await dirBytes(dir, budget);
8253
8429
  for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs >= this.startedAt - 1e3)) {
8254
- bytes += await dirBytes(path23.join(this.tmpDir, name), budget);
8430
+ bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
8255
8431
  }
8256
8432
  if (budget.files <= 0) this.peakInUseIsLowerBound = true;
8257
8433
  this.peakInUse = Math.max(this.peakInUse ?? 0, bytes);
@@ -8268,7 +8444,7 @@ var RunSampler = class {
8268
8444
  for (const name of names) {
8269
8445
  if (!PLAYWRIGHT_TEMP.test(name)) continue;
8270
8446
  try {
8271
- if (keep((await fs21.promises.stat(path23.join(this.tmpDir, name))).mtimeMs)) out.push(name);
8447
+ if (keep((await fs21.promises.stat(path21.join(this.tmpDir, name))).mtimeMs)) out.push(name);
8272
8448
  } catch {
8273
8449
  }
8274
8450
  }
@@ -8279,7 +8455,7 @@ var RunSampler = class {
8279
8455
  const budget = { files: WALK_FILE_CAP };
8280
8456
  let bytes = 0;
8281
8457
  for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs < this.startedAt - 1e3)) {
8282
- bytes += await dirBytes(path23.join(this.tmpDir, name), budget);
8458
+ bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
8283
8459
  }
8284
8460
  return bytes;
8285
8461
  } catch {
@@ -8672,7 +8848,7 @@ function resourceTimeline(samples, censuses) {
8672
8848
  let startedAt = hasSamples ? samples.startedAt : Infinity;
8673
8849
  for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
8674
8850
  const shift = hasSamples ? samples.startedAt - startedAt : 0;
8675
- const relative15 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
8851
+ const relative11 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
8676
8852
  const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
8677
8853
  const perWorker = Math.max(
8678
8854
  60,
@@ -8680,17 +8856,17 @@ function resourceTimeline(samples, censuses) {
8680
8856
  );
8681
8857
  return {
8682
8858
  startedAt,
8683
- cpuPct: hasSamples ? downsamplePoints(relative15(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
8684
- memoryBytes: hasSamples ? downsamplePoints(relative15(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
8859
+ cpuPct: hasSamples ? downsamplePoints(relative11(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
8860
+ memoryBytes: hasSamples ? downsamplePoints(relative11(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
8685
8861
  pages: workers.map(({ worker, events }) => {
8686
- const points = relative15(events, -startedAt);
8862
+ const points = relative11(events, -startedAt);
8687
8863
  const last = points.pop();
8688
8864
  return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
8689
8865
  }),
8690
8866
  workers: trees.map((tree) => ({
8691
8867
  worker: tree.worker,
8692
- cpuCores: downsamplePoints(relative15(tree.cpuCores, shift), perWorker, "mean"),
8693
- memoryBytes: downsamplePoints(relative15(tree.memoryBytes, shift), perWorker, "max")
8868
+ cpuCores: downsamplePoints(relative11(tree.cpuCores, shift), perWorker, "mean"),
8869
+ memoryBytes: downsamplePoints(relative11(tree.memoryBytes, shift), perWorker, "max")
8694
8870
  }))
8695
8871
  };
8696
8872
  }
@@ -9252,7 +9428,7 @@ function browserFunctionsInWorker() {
9252
9428
  function browserFunctionCoverage(files) {
9253
9429
  return sanitizeCoverage({
9254
9430
  v: 1,
9255
- files: [...files].map(([path34, functions]) => ({ path: path34, runtime: "browser", functions }))
9431
+ files: [...files].map(([path32, functions]) => ({ path: path32, runtime: "browser", functions }))
9256
9432
  });
9257
9433
  }
9258
9434
  function newKnownFunctions(known, sent) {
@@ -9265,19 +9441,19 @@ function newKnownFunctions(known, sent) {
9265
9441
  budget = MAX_COVERAGE_RANGES;
9266
9442
  };
9267
9443
  for (const [raw, ranges] of known) {
9268
- const path34 = normalizeCoveragePath(raw);
9269
- if (path34 === null) continue;
9270
- let named = sent.get(path34);
9444
+ const path32 = normalizeCoveragePath(raw);
9445
+ if (path32 === null) continue;
9446
+ let named = sent.get(path32);
9271
9447
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
9272
9448
  if (fresh.length === 0) continue;
9273
- if (!named) sent.set(path34, named = /* @__PURE__ */ new Set());
9449
+ if (!named) sent.set(path32, named = /* @__PURE__ */ new Set());
9274
9450
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
9275
9451
  while (fresh.length > 0) {
9276
9452
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
9277
9453
  const part = fresh.slice(0, budget);
9278
9454
  fresh = fresh.slice(part.length);
9279
9455
  budget -= part.length;
9280
- files.push({ path: path34, allFunctions: part });
9456
+ files.push({ path: path32, allFunctions: part });
9281
9457
  }
9282
9458
  }
9283
9459
  close();
@@ -9288,9 +9464,9 @@ function serverCoverageInWorker() {
9288
9464
  }
9289
9465
 
9290
9466
  // src/internal/capture/code-reach.ts
9291
- var crypto4 = __toESM(require("crypto"));
9467
+ var crypto5 = __toESM(require("crypto"));
9292
9468
  var fs22 = __toESM(require("fs"));
9293
- var path24 = __toESM(require("path"));
9469
+ var path22 = __toESM(require("path"));
9294
9470
  var import_node_child_process3 = require("child_process");
9295
9471
 
9296
9472
  // ../core/src/source-map.ts
@@ -9434,14 +9610,6 @@ function decodeSourceMap(raw) {
9434
9610
  return originalAt(line, column)?.source ?? null;
9435
9611
  },
9436
9612
  originalAt,
9437
- mapsBetween(source, start, end) {
9438
- let found = false;
9439
- eachSegmentBetween(segments, start, end, (i) => {
9440
- found = segments.sources[i] === source;
9441
- return found;
9442
- });
9443
- return found;
9444
- },
9445
9613
  firstOriginalLineBetween(source, start, end) {
9446
9614
  let first = null;
9447
9615
  eachSegmentBetween(segments, start, end, (i) => {
@@ -9643,12 +9811,12 @@ function repositoryRoot(from) {
9643
9811
  }).trim();
9644
9812
  } catch {
9645
9813
  }
9646
- cachedRepoRoot = { from, root: path24.resolve(root) };
9814
+ cachedRepoRoot = { from, root: path22.resolve(root) };
9647
9815
  return cachedRepoRoot.root;
9648
9816
  }
9649
9817
  function codeReachRoots(configDir, configured) {
9650
9818
  const repoRoot = repositoryRoot(configDir);
9651
- const resolve9 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path24.resolve(configDir, r));
9819
+ const resolve9 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path22.resolve(configDir, r));
9652
9820
  const global = Array.isArray(configured) ? resolve9(configured) : resolve9(configured?.["*"]);
9653
9821
  const byOrigin = /* @__PURE__ */ new Map();
9654
9822
  if (configured && !Array.isArray(configured)) {
@@ -9685,10 +9853,10 @@ function isFile(file) {
9685
9853
  function resolveSourcePath(source, roots) {
9686
9854
  const normalized = normalizeSourcePath(source);
9687
9855
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
9688
- if (path24.isAbsolute(normalized) && isFile(normalized)) return path24.resolve(normalized);
9689
- const relative15 = normalized.replace(/^\/+/, "");
9856
+ if (path22.isAbsolute(normalized) && isFile(normalized)) return path22.resolve(normalized);
9857
+ const relative11 = normalized.replace(/^\/+/, "");
9690
9858
  for (const root of roots) {
9691
- const candidate = path24.resolve(root, relative15);
9859
+ const candidate = path22.resolve(root, relative11);
9692
9860
  if (isFile(candidate)) return candidate;
9693
9861
  }
9694
9862
  return null;
@@ -9720,10 +9888,10 @@ function fileForUrl(url, roots, bases) {
9720
9888
  return file;
9721
9889
  }
9722
9890
  function relativeToRepo(file, repoRoot) {
9723
- return path24.relative(repoRoot, file).split(path24.sep).join("/");
9891
+ return path22.relative(repoRoot, file).split(path22.sep).join("/");
9724
9892
  }
9725
9893
  function scriptVersion(entry) {
9726
- return entry.hash ?? crypto4.createHash("sha1").update(entry.source ?? "").digest("hex");
9894
+ return entry.hash ?? crypto5.createHash("sha1").update(entry.source ?? "").digest("hex");
9727
9895
  }
9728
9896
  async function loadMap(entry, fetchMap) {
9729
9897
  const ref = entry.source ? sourceMappingUrl(entry.source) : null;
@@ -9777,10 +9945,10 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
9777
9945
  const files = /* @__PURE__ */ new Map();
9778
9946
  const known = /* @__PURE__ */ new Map();
9779
9947
  const addTo = (into, file, functions) => {
9780
- const path34 = relativeToRepo(file, roots.repoRoot);
9781
- const listed = into.get(path34);
9948
+ const path32 = relativeToRepo(file, roots.repoRoot);
9949
+ const listed = into.get(path32);
9782
9950
  if (listed) listed.push(...functions);
9783
- else into.set(path34, [...functions]);
9951
+ else into.set(path32, [...functions]);
9784
9952
  };
9785
9953
  const add = (file, functions) => addTo(files, file, functions);
9786
9954
  const bases = viteBases(entries);
@@ -10044,11 +10212,11 @@ async function stopCodeReach(page) {
10044
10212
 
10045
10213
  // src/internal/collect/blob-ids.ts
10046
10214
  var import_node_child_process4 = require("child_process");
10047
- var crypto5 = __toESM(require("crypto"));
10215
+ var crypto6 = __toESM(require("crypto"));
10048
10216
  var fs23 = __toESM(require("fs"));
10049
- var path25 = __toESM(require("path"));
10217
+ var path23 = __toESM(require("path"));
10050
10218
  function gitBlobId(content) {
10051
- return crypto5.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
10219
+ return crypto6.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
10052
10220
  }
10053
10221
  function namesLines(file) {
10054
10222
  return Boolean(file.functions?.length || file.lines?.length || file.executable?.length || file.allFunctions?.length);
@@ -10113,14 +10281,14 @@ var BlobIds = class {
10113
10281
  async read(relativePaths) {
10114
10282
  let root;
10115
10283
  try {
10116
- root = path25.resolve(this.repoRoot());
10284
+ root = path23.resolve(this.repoRoot());
10117
10285
  } catch {
10118
10286
  return relativePaths.map(() => null);
10119
10287
  }
10120
10288
  const files = await Promise.all(
10121
10289
  relativePaths.map(async (relativePath2) => {
10122
- const file = path25.resolve(root, relativePath2);
10123
- if (!file.startsWith(root + path25.sep)) return null;
10290
+ const file = path23.resolve(root, relativePath2);
10291
+ if (!file.startsWith(root + path23.sep)) return null;
10124
10292
  const stat = await fs23.promises.stat(file).catch(() => null);
10125
10293
  return stat?.isFile() ? file : null;
10126
10294
  })
@@ -10129,7 +10297,7 @@ var BlobIds = class {
10129
10297
  const fromGit = /* @__PURE__ */ new Map();
10130
10298
  const ids = forGit.length > 0 ? await this.hashObjects(
10131
10299
  root,
10132
- forGit.map((file) => path25.relative(root, file))
10300
+ forGit.map((file) => path23.relative(root, file))
10133
10301
  ) : null;
10134
10302
  if (ids) forGit.forEach((file, i) => fromGit.set(file, ids[i]));
10135
10303
  return Promise.all(
@@ -10398,17 +10566,26 @@ var CoverageCollection = class {
10398
10566
  }
10399
10567
  /**
10400
10568
  * Send the run-level coverage of every origin, and the browser's, to the
10401
- * dashboard run, in requests within its limits. A failure is logged once,
10402
- * never thrown.
10569
+ * dashboard run, in requests within its limits; the first names the origins
10570
+ * whose server coverage is incomplete (`incomplete`), and is sent for them
10571
+ * alone when no file is. A failure is logged once, never thrown.
10403
10572
  */
10404
10573
  async sendRunCoverage(httpClient, runId, auth) {
10405
10574
  if (!this.collector && !this.browserBlobs) return;
10406
10575
  this.fetchRunCoverage();
10407
- const server = (await Promise.all(this.runFiles.values())).flatMap((result) => result ?? []);
10576
+ const runs = await Promise.all(this.runFiles.values());
10577
+ const server = runs.flatMap((result) => result?.files ?? []);
10578
+ const gaps = runs.flatMap((result) => result?.gap ? [result.gap] : []);
10408
10579
  const files = [...server, ...await this.browserRunFiles()];
10409
- for (const chunk of chunkRunCoverage(files)) {
10580
+ const chunks = chunkRunCoverage(files);
10581
+ if (chunks.length === 0 && gaps.length > 0) chunks.push([]);
10582
+ for (const [i, chunk] of chunks.entries()) {
10410
10583
  try {
10411
- await httpClient.putJSON(`/api/test-runs/${runId}/coverage`, { v: 1, files: chunk }, auth);
10584
+ await httpClient.putJSON(
10585
+ `/api/test-runs/${runId}/coverage`,
10586
+ { v: 1, files: chunk, ...i === 0 && gaps.length > 0 ? { incomplete: gaps } : {} },
10587
+ auth
10588
+ );
10412
10589
  } catch (error) {
10413
10590
  this.logger.warn(`Could not send the run's coverage to the dashboard: ${errorMessage(error)}`);
10414
10591
  return;
@@ -10425,11 +10602,11 @@ var CoverageCollection = class {
10425
10602
  };
10426
10603
 
10427
10604
  // src/public/reporter.ts
10428
- function testLocation(test) {
10429
- return `${testFile(test)}:${test.location.line}:${test.location.column}`;
10605
+ function testLocation(test, base) {
10606
+ return `${testFile(test, base)}:${test.location.line}:${test.location.column}`;
10430
10607
  }
10431
- function testFile(test) {
10432
- return path26.relative(process.cwd(), test.location.file).split(path26.sep).join("/");
10608
+ function testFile(test, base) {
10609
+ return relativeTestPath(test.location.file, base);
10433
10610
  }
10434
10611
  var PiwiDashboardReporter = class _PiwiDashboardReporter {
10435
10612
  constructor(rawOptions = {}) {
@@ -10449,6 +10626,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10449
10626
  this.reportedTestIds = /* @__PURE__ */ new Set();
10450
10627
  /** Per-test attempt history, keyed by `test.id`; snapshotted onto every attempt's payload. */
10451
10628
  this.attemptsByTest = /* @__PURE__ */ new Map();
10629
+ /** The folder test files are named relative to: the Playwright config's, set in `onBegin`. */
10630
+ this.testPathBase = process.cwd();
10452
10631
  /** Derived in `onBegin`, once the shard is known. */
10453
10632
  this.instanceId = "";
10454
10633
  this.shardInfo = null;
@@ -10492,7 +10671,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10492
10671
  this.httpClient = new HttpClient(this.options.serverUrl ?? "http://localhost:3000", logger);
10493
10672
  this.fileHandler = new FileHandler(logger);
10494
10673
  this.uploader = new Uploader(this.httpClient, this.fileHandler, logger);
10495
- this.recovery = new CrashRecovery(this.options.projectName, logger);
10674
+ this.recovery = new CrashRecovery(this.httpClient.baseUrl, this.options.projectName, logger);
10496
10675
  this.metadataCollector = new MetadataCollector(logger);
10497
10676
  this.failureLinks = new FailureLinks(this.httpClient.baseUrl, logger);
10498
10677
  const streamBuffer = new StreamBuffer(this.options.projectName);
@@ -10523,6 +10702,15 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10523
10702
  static {
10524
10703
  this.createGlobalSetup = createGlobalSetup;
10525
10704
  }
10705
+ /**
10706
+ * Playwright reporter hook: whether this reporter writes the run's progress to
10707
+ * the terminal. It does not (it prints only its own `[Piwi Dashboard]` lines),
10708
+ * so Playwright keeps its own terminal reporter when Piwi is the only one
10709
+ * configured.
10710
+ */
10711
+ printsToStdio() {
10712
+ return false;
10713
+ }
10526
10714
  /** Playwright reporter hook: called once at the start of the test run */
10527
10715
  onBegin(config, suite) {
10528
10716
  if (this.listMode) {
@@ -10537,10 +10725,11 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10537
10725
  this.logger.info(`Using the desktop app running at ${this.options.serverUrl} (no serverUrl configured).`);
10538
10726
  }
10539
10727
  this.startTime = (/* @__PURE__ */ new Date()).toISOString();
10728
+ this.testPathBase = configDirFromConfig(config);
10540
10729
  this.playwrightVersion = config.version;
10541
10730
  this.maxFailures = config.maxFailures ?? 0;
10542
10731
  if (this.options.captureResources !== false) {
10543
- this.resourcesFile = path26.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
10732
+ this.resourcesFile = path24.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
10544
10733
  process.env[PIWI_RESOURCES_RESULTS_ENV] = this.resourcesFile;
10545
10734
  try {
10546
10735
  this.sampler = new RunSampler({ outputDirs: (config.projects ?? []).map((project) => project.outputDir) });
@@ -10642,7 +10831,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10642
10831
  const beginEvent = {
10643
10832
  type: "begin",
10644
10833
  title: test.title,
10645
- location: testLocation(test),
10834
+ location: testLocation(test, this.testPathBase),
10646
10835
  workerIndex: workerIndexOf(result),
10647
10836
  shardIndex: this.shardInfo?.current ?? null,
10648
10837
  browser: this.metadataCollector.getBrowserConfig(test) || void 0,
@@ -10738,8 +10927,9 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10738
10927
  this.attemptsByTest.set(test.id, attempts);
10739
10928
  const testCase = {
10740
10929
  type: "complete",
10930
+ testId: test.id,
10741
10931
  title: test.title,
10742
- location: testLocation(test),
10932
+ location: testLocation(test, this.testPathBase),
10743
10933
  status,
10744
10934
  duration: result.duration,
10745
10935
  // Effective per-test timeout (reflects project config + describe-level
@@ -10747,7 +10937,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10747
10937
  timeout: test.timeout ?? null,
10748
10938
  // A `test.fail()` test that passed is now `failed` with no recorded error;
10749
10939
  // Playwright reports the same line, so synthesize it.
10750
- error: expectedFailureError(result.status, annotations) ?? buildErrorText(result),
10940
+ error: expectedFailureError(result.status, annotations) ?? buildErrorText(result, this.testPathBase),
10751
10941
  retries: result.retry,
10752
10942
  attempts: attempts.map((a) => ({ ...a })),
10753
10943
  workerIndex: workerIndexOf(result),
@@ -10831,7 +11021,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10831
11021
  if (isFailure && result.retry >= (test.retries ?? 0)) {
10832
11022
  this.failureLinks.add({
10833
11023
  title: test.title,
10834
- file: testFile(test),
11024
+ file: testFile(test, this.testPathBase),
10835
11025
  retry: result.retry,
10836
11026
  browser: typeof testCase.browser?.projectName === "string" ? testCase.browser.projectName : null,
10837
11027
  headline: failureHeadline(testCase.error, testCase.performanceMetrics?.steps)
@@ -10881,7 +11071,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10881
11071
  return;
10882
11072
  }
10883
11073
  if (!result.steps?.length) return;
10884
- const tally = tallyLifecycleSteps(testFile(test), result.steps);
11074
+ const tally = tallyLifecycleSteps(testFile(test, this.testPathBase), result.steps);
10885
11075
  if (tally.opened.length > 0 || tally.closed.context > 0 || tally.closed.browser > 0)
10886
11076
  this.lifecycleTallies.push(tally);
10887
11077
  }
@@ -10944,8 +11134,9 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10944
11134
  const locks = collectTestLocks(test);
10945
11135
  const testCase = {
10946
11136
  type: "complete",
11137
+ testId: test.id,
10947
11138
  title: test.title,
10948
- location: testLocation(test),
11139
+ location: testLocation(test, this.testPathBase),
10949
11140
  status: "didnotrun",
10950
11141
  duration: 0,
10951
11142
  timeout: test.timeout ?? null,
@@ -11022,6 +11213,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11022
11213
  const branch = resolveScmBranch(process.env) ?? null;
11023
11214
  await this.manifests.upload(this.httpClient, submitted.auth, this.options.projectName, branch);
11024
11215
  }
11216
+ } catch (error) {
11217
+ this.logger.error(`Could not report the run to the dashboard: ${errorMessage(error)}`);
11025
11218
  } finally {
11026
11219
  this.coverage?.close();
11027
11220
  this.fileHandler.cleanupBodyAttachments();
@@ -11103,7 +11296,7 @@ async function resolveSelection(options = {}) {
11103
11296
  }
11104
11297
 
11105
11298
  // src/internal/capture/capture-fixtures.ts
11106
- var path30 = __toESM(require("path"));
11299
+ var path28 = __toESM(require("path"));
11107
11300
  var import_node_zlib2 = require("zlib");
11108
11301
 
11109
11302
  // ../picker-dom/src/probe.ts
@@ -11534,26 +11727,26 @@ function installPickerOverlay(arg) {
11534
11727
  }
11535
11728
  };
11536
11729
  mount(highlight, label, banner);
11537
- const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
11730
+ const quoteJs = (s) => `'${JSON.stringify(s).slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
11538
11731
  const describe = (el) => {
11539
11732
  const tag = (el.tagName || "?").toLowerCase();
11540
11733
  const testId = el.getAttribute && el.getAttribute(testIdAttr);
11541
- if (testId) return `getByTestId('${escJs(testId)}')`;
11734
+ if (testId) return `getByTestId(${quoteJs(testId)})`;
11542
11735
  if (el.labels && el.labels.length > 0) {
11543
11736
  const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
11544
- if (labelText) return `getByLabel('${escJs(labelText)}')`;
11737
+ if (labelText) return `getByLabel(${quoteJs(labelText)})`;
11545
11738
  }
11546
11739
  const ariaLabel = el.getAttribute && el.getAttribute("aria-label");
11547
- if (ariaLabel) return `getByLabel('${escJs(ariaLabel)}')`;
11740
+ if (ariaLabel) return `getByLabel(${quoteJs(ariaLabel)})`;
11548
11741
  const placeholder = el.getAttribute && el.getAttribute("placeholder");
11549
- if (placeholder) return `getByPlaceholder('${escJs(placeholder)}')`;
11742
+ if (placeholder) return `getByPlaceholder(${quoteJs(placeholder)})`;
11550
11743
  const alt = el.getAttribute && el.getAttribute("alt");
11551
- if (alt) return `getByAltText('${escJs(alt)}')`;
11744
+ if (alt) return `getByAltText(${quoteJs(alt)})`;
11552
11745
  const titleAttr = el.getAttribute && el.getAttribute("title");
11553
- if (titleAttr) return `getByTitle('${escJs(titleAttr)}')`;
11554
- if (el.id) return `locator('#${escJs(el.id)}')`;
11746
+ if (titleAttr) return `getByTitle(${quoteJs(titleAttr)})`;
11747
+ if (el.id) return `locator(${quoteJs(`#${el.id}`)})`;
11555
11748
  const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
11556
- return cls ? `locator('.${escJs(cls)}')` : tag;
11749
+ return cls ? `locator(${quoteJs(`.${cls}`)})` : tag;
11557
11750
  };
11558
11751
  const buildChain = (raw) => {
11559
11752
  const chain2 = [];
@@ -12339,7 +12532,6 @@ function headingLevel(attrs, role) {
12339
12532
  return null;
12340
12533
  }
12341
12534
  var attr = (a, key) => a.attributes[key] || null;
12342
- var esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
12343
12535
  var escCssAttrValue = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
12344
12536
  var isCssSafeId = (id) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(id);
12345
12537
  function classifyCssStability(className) {
@@ -12394,7 +12586,7 @@ function generateAlternatives(attrs, options = {}) {
12394
12586
  const testId = attr(attrs, testIdAttribute);
12395
12587
  if (testId && isUnique(counts?.testId)) {
12396
12588
  add({
12397
- locator: `getByTestId('${esc(testId)}')`,
12589
+ locator: `getByTestId(${quoteJsString(testId)})`,
12398
12590
  method: "getByTestId",
12399
12591
  args: { testId },
12400
12592
  score: 100
@@ -12405,7 +12597,7 @@ function generateAlternatives(attrs, options = {}) {
12405
12597
  const withLevel = (base) => level != null ? { ...base, level } : base;
12406
12598
  if (role && accessibleName) {
12407
12599
  add({
12408
- locator: `getByRole('${role}', { name: '${esc(accessibleName)}'${levelPart} })`,
12600
+ locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(accessibleName)}${levelPart} })`,
12409
12601
  method: "getByRole",
12410
12602
  args: withLevel({ role, name: accessibleName }),
12411
12603
  score: 90 - ambiguityPenalty(counts?.roleName)
@@ -12414,7 +12606,7 @@ function generateAlternatives(attrs, options = {}) {
12414
12606
  const ariaLabel = attr(attrs, "aria-label");
12415
12607
  if (role && ariaLabel && ariaLabel !== accessibleName) {
12416
12608
  add({
12417
- locator: `getByRole('${role}', { name: '${esc(ariaLabel)}'${levelPart} })`,
12609
+ locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(ariaLabel)}${levelPart} })`,
12418
12610
  method: "getByRole",
12419
12611
  args: withLevel({ role, name: ariaLabel }),
12420
12612
  score: 85 - ambiguityPenalty(counts?.roleName)
@@ -12425,7 +12617,7 @@ function generateAlternatives(attrs, options = {}) {
12425
12617
  const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
12426
12618
  if (labelBacked) {
12427
12619
  add({
12428
- locator: `getByLabel('${esc(accessibleName)}')`,
12620
+ locator: `getByLabel(${quoteJsString(accessibleName)})`,
12429
12621
  method: "getByLabel",
12430
12622
  args: { label: accessibleName },
12431
12623
  score: 85
@@ -12435,7 +12627,7 @@ function generateAlternatives(attrs, options = {}) {
12435
12627
  const placeholder = attr(attrs, "placeholder");
12436
12628
  if (placeholder) {
12437
12629
  add({
12438
- locator: `getByPlaceholder('${esc(placeholder)}')`,
12630
+ locator: `getByPlaceholder(${quoteJsString(placeholder)})`,
12439
12631
  method: "getByPlaceholder",
12440
12632
  args: { placeholder },
12441
12633
  score: 80 - ambiguityPenalty(counts?.placeholder)
@@ -12443,7 +12635,7 @@ function generateAlternatives(attrs, options = {}) {
12443
12635
  }
12444
12636
  if (text && text.length < 80) {
12445
12637
  add({
12446
- locator: `getByText('${esc(text)}')`,
12638
+ locator: `getByText(${quoteJsString(text)})`,
12447
12639
  method: "getByText",
12448
12640
  args: { text },
12449
12641
  score: 75 - ambiguityPenalty(counts?.text)
@@ -12453,7 +12645,7 @@ function generateAlternatives(attrs, options = {}) {
12453
12645
  if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
12454
12646
  const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
12455
12647
  add({
12456
- locator: `locator('${esc(selector)}')`,
12648
+ locator: `locator(${quoteJsString(selector)})`,
12457
12649
  method: "locator",
12458
12650
  args: { selector },
12459
12651
  score: 65
@@ -12463,7 +12655,7 @@ function generateAlternatives(attrs, options = {}) {
12463
12655
  if (name && selectable("name") && isUnique(counts?.name)) {
12464
12656
  const selector = `[name="${escCssAttrValue(name)}"]`;
12465
12657
  add({
12466
- locator: `locator('${esc(selector)}')`,
12658
+ locator: `locator(${quoteJsString(selector)})`,
12467
12659
  method: "locator",
12468
12660
  args: { selector },
12469
12661
  score: 60
@@ -12472,7 +12664,7 @@ function generateAlternatives(attrs, options = {}) {
12472
12664
  const alt = attr(attrs, "alt");
12473
12665
  if (alt) {
12474
12666
  add({
12475
- locator: `getByAltText('${esc(alt)}')`,
12667
+ locator: `getByAltText(${quoteJsString(alt)})`,
12476
12668
  method: "getByAltText",
12477
12669
  args: { text: alt },
12478
12670
  score: 60 - ambiguityPenalty(counts?.alt)
@@ -12481,7 +12673,7 @@ function generateAlternatives(attrs, options = {}) {
12481
12673
  const title = attr(attrs, "title");
12482
12674
  if (title) {
12483
12675
  add({
12484
- locator: `getByTitle('${esc(title)}')`,
12676
+ locator: `getByTitle(${quoteJsString(title)})`,
12485
12677
  method: "getByTitle",
12486
12678
  args: { title },
12487
12679
  score: 50 - ambiguityPenalty(counts?.title)
@@ -12499,7 +12691,7 @@ function generateAlternatives(attrs, options = {}) {
12499
12691
  if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
12500
12692
  testIdAnchorDone = true;
12501
12693
  add({
12502
- locator: `getByTestId('${esc(anc.testId)}').${leaf.expr}`,
12694
+ locator: `getByTestId(${quoteJsString(anc.testId)}).${leaf.expr}`,
12503
12695
  method: leaf.method,
12504
12696
  args: { ...leaf.args, anchorTestId: anc.testId },
12505
12697
  score: scores.testId
@@ -12509,7 +12701,7 @@ function generateAlternatives(attrs, options = {}) {
12509
12701
  idAnchorDone = true;
12510
12702
  const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
12511
12703
  add({
12512
- locator: `locator('${esc(anchorSelector)}').${leaf.expr}`,
12704
+ locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
12513
12705
  method: leaf.method,
12514
12706
  args: { ...leaf.args, anchorSelector },
12515
12707
  score: scores.id
@@ -12520,7 +12712,7 @@ function generateAlternatives(attrs, options = {}) {
12520
12712
  const { name: name2, value } = anc.dataAttr;
12521
12713
  const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
12522
12714
  add({
12523
- locator: `locator('${esc(anchorSelector)}').${leaf.expr}`,
12715
+ locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
12524
12716
  method: leaf.method,
12525
12717
  args: { ...leaf.args, anchorSelector },
12526
12718
  score: TEST_DATA_ATTRS.has(name2) ? scores.testData : scores.data
@@ -12530,7 +12722,7 @@ function generateAlternatives(attrs, options = {}) {
12530
12722
  if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
12531
12723
  roleAnchorDone = true;
12532
12724
  add({
12533
- locator: `getByRole('${esc(ancestorRole)}').${leaf.expr}`,
12725
+ locator: `getByRole(${quoteJsString(ancestorRole)}).${leaf.expr}`,
12534
12726
  method: leaf.method,
12535
12727
  args: { ...leaf.args, anchorRole: ancestorRole },
12536
12728
  score: scores.role
@@ -12539,7 +12731,7 @@ function generateAlternatives(attrs, options = {}) {
12539
12731
  if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
12540
12732
  filterAnchorDone = true;
12541
12733
  add({
12542
- locator: `getByRole('${esc(ancestorRole)}').filter({ hasText: '${esc(anc.filterText)}' }).${leaf.expr}`,
12734
+ locator: `getByRole(${quoteJsString(ancestorRole)}).filter({ hasText: ${quoteJsString(anc.filterText)} }).${leaf.expr}`,
12543
12735
  method: leaf.method,
12544
12736
  args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
12545
12737
  score: scores.filter
@@ -12548,7 +12740,7 @@ function generateAlternatives(attrs, options = {}) {
12548
12740
  }
12549
12741
  };
12550
12742
  if (role && !hasOwnTestId) {
12551
- const rolePart = level != null ? `'${role}', { level: ${level} }` : `'${role}'`;
12743
+ const rolePart = level != null ? `${quoteJsString(role)}, { level: ${level} }` : quoteJsString(role);
12552
12744
  const leafArgs = withLevel({ role });
12553
12745
  addAnchoredChains(
12554
12746
  { expr: `getByRole(${rolePart})`, method: "getByRole", args: leafArgs, role },
@@ -12566,7 +12758,7 @@ function generateAlternatives(attrs, options = {}) {
12566
12758
  }
12567
12759
  } else if (!role && text && text.length < 80 && !hasOwnTestId) {
12568
12760
  addAnchoredChains(
12569
- { expr: `getByText('${esc(text)}')`, method: "getByText", args: { text }, role: null },
12761
+ { expr: `getByText(${quoteJsString(text)})`, method: "getByText", args: { text }, role: null },
12570
12762
  (anc) => anc.scopedTextCount,
12571
12763
  { testId: 68, testData: 66, id: 60, data: 56, role: 51, filter: 49 }
12572
12764
  );
@@ -12577,7 +12769,7 @@ function generateAlternatives(attrs, options = {}) {
12577
12769
  const classes = clsStr.split(/\s+/).filter((c) => c.length > 1 && /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(c) && isUnique(countsClasses?.[c])).map((cls) => ({ cls, score: classifyCssStability(cls) })).sort((a, b) => b.score - a.score).slice(0, 3);
12578
12770
  for (const { cls, score } of classes) {
12579
12771
  add({
12580
- locator: `locator('.${esc(cls)}')`,
12772
+ locator: `locator(${quoteJsString(`.${cls}`)})`,
12581
12773
  method: "locator",
12582
12774
  args: { selector: `.${cls}` },
12583
12775
  score
@@ -12605,26 +12797,25 @@ function approximateAccessibleName(attrs) {
12605
12797
  }
12606
12798
 
12607
12799
  // ../picker-dom/src/anchor-alternatives.ts
12608
- var escStr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
12609
12800
  var cssIdSelector = (id) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id) ? `#${id}` : `[id="${id.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"]`;
12610
12801
  var ANCHOR_KIND_SCORES = { testid: 80, id: 74, labeledRole: 68, role: 62 };
12611
12802
  function anchorSegment(a) {
12612
12803
  if (a.testId && a.testIdCount === 1) {
12613
- return { code: `getByTestId('${escStr(a.testId)}')`, kind: "testid", flatArgs: { anchorTestId: a.testId } };
12804
+ return { code: `getByTestId(${quoteJsString(a.testId)})`, kind: "testid", flatArgs: { anchorTestId: a.testId } };
12614
12805
  }
12615
12806
  if (a.id && !isAutoGenerated(a.id) && a.idCount === 1) {
12616
12807
  const anchorSelector = cssIdSelector(a.id);
12617
- return { code: `locator('${escStr(anchorSelector)}')`, kind: "id", flatArgs: { anchorSelector } };
12808
+ return { code: `locator(${quoteJsString(anchorSelector)})`, kind: "id", flatArgs: { anchorSelector } };
12618
12809
  }
12619
12810
  if (a.role && a.ariaLabel && a.labeledRoleCount === 1) {
12620
12811
  return {
12621
- code: `getByRole('${escStr(a.role)}', { name: '${escStr(a.ariaLabel)}' })`,
12812
+ code: `getByRole(${quoteJsString(a.role)}, { name: ${quoteJsString(a.ariaLabel)} })`,
12622
12813
  kind: "labeledRole",
12623
12814
  flatArgs: { anchorRole: a.role, anchorName: a.ariaLabel }
12624
12815
  };
12625
12816
  }
12626
12817
  if (a.role && a.roleCount === 1) {
12627
- return { code: `getByRole('${escStr(a.role)}')`, kind: "role", flatArgs: { anchorRole: a.role } };
12818
+ return { code: `getByRole(${quoteJsString(a.role)})`, kind: "role", flatArgs: { anchorRole: a.role } };
12628
12819
  }
12629
12820
  return null;
12630
12821
  }
@@ -12632,7 +12823,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
12632
12823
  if (!leaf.role || anchors.length === 0) return [];
12633
12824
  const out = [];
12634
12825
  const seen = /* @__PURE__ */ new Set();
12635
- const rolePart = leaf.level != null ? `'${leaf.role}', { level: ${leaf.level} }` : `'${leaf.role}'`;
12826
+ const rolePart = leaf.level != null ? `${quoteJsString(leaf.role)}, { level: ${leaf.level} }` : quoteJsString(leaf.role);
12636
12827
  const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
12637
12828
  const add = (l) => {
12638
12829
  if (!seen.has(l.locator)) {
@@ -12687,21 +12878,21 @@ var MAX_INDETERMINATE = 20;
12687
12878
  var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
12688
12879
  var LEVEL = `${STEP}(?:/${STEP})*`;
12689
12880
  var ELEMENT_PATH_RE = new RegExp(`^${LEVEL}(?:\\|${LEVEL})*$`);
12690
- function isElementPath(path34) {
12691
- return typeof path34 === "string" && path34.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path34);
12881
+ function isElementPath(path32) {
12882
+ return typeof path32 === "string" && path32.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path32);
12692
12883
  }
12693
12884
  var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
12694
12885
  function elementRef(v) {
12695
12886
  if (!v || typeof v !== "object") return null;
12696
- const { path: path34, label } = v;
12697
- if (!isElementPath(path34)) return null;
12698
- return { path: path34, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
12887
+ const { path: path32, label } = v;
12888
+ if (!isElementPath(path32)) return null;
12889
+ return { path: path32, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
12699
12890
  }
12700
12891
  function textPoint(v) {
12701
12892
  if (!v || typeof v !== "object") return null;
12702
- const { path: path34, offset } = v;
12893
+ const { path: path32, offset } = v;
12703
12894
  const at = offsetOf(offset);
12704
- return isElementPath(path34) && at !== null ? { path: path34, offset: at } : null;
12895
+ return isElementPath(path32) && at !== null ? { path: path32, offset: at } : null;
12705
12896
  }
12706
12897
  function sanitizePageInteraction(raw) {
12707
12898
  if (!raw || typeof raw !== "object") return {};
@@ -12732,7 +12923,7 @@ function sanitizePageInteraction(raw) {
12732
12923
  }
12733
12924
 
12734
12925
  // src/internal/capture/locator-healing.ts
12735
- var path27 = __toESM(require("path"));
12926
+ var path25 = __toESM(require("path"));
12736
12927
 
12737
12928
  // ../core/src/locator-fingerprint.ts
12738
12929
  var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
@@ -12758,7 +12949,6 @@ var TEXT_CONTENT_ROLES = /* @__PURE__ */ new Set([
12758
12949
  "switch"
12759
12950
  ]);
12760
12951
  var FORM_FIELD_ROLES = /* @__PURE__ */ new Set(["textbox", "combobox", "searchbox", "spinbutton", "slider"]);
12761
- var escapeQuote = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
12762
12952
  function parseAriaCandidates(ariaSnapshot) {
12763
12953
  if (!ariaSnapshot) return [];
12764
12954
  const out = [];
@@ -12786,6 +12976,23 @@ function textSimilarity(a, b) {
12786
12976
  for (const t of sa) if (sb.has(t)) common++;
12787
12977
  return 2 * common / (sa.size + sb.size);
12788
12978
  }
12979
+ var FUNCTION_WORDS = new Set(
12980
+ "the and for with from your you our this that les des une pour avec par sur aux dans vos votre nos notre".split(" ")
12981
+ );
12982
+ function significantWords(text) {
12983
+ const all = (text ?? "").replace(new RegExp("(\\p{Ll})(\\p{Lu})", "gu"), "$1 $2").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
12984
+ const meaningful = all.filter((w) => w.length >= 3 && !FUNCTION_WORDS.has(w));
12985
+ return new Set(meaningful.length > 0 ? meaningful : all);
12986
+ }
12987
+ function significantWordSimilarity(a, b) {
12988
+ const sa = significantWords(a);
12989
+ const sb = significantWords(b);
12990
+ if (sa.size === 0 && sb.size === 0) return 1;
12991
+ if (sa.size === 0 || sb.size === 0) return 0;
12992
+ let common = 0;
12993
+ for (const w of sa) if (sb.has(w)) common++;
12994
+ return 2 * common / (sa.size + sb.size);
12995
+ }
12789
12996
  function fingerprintPresent(fp, candidates) {
12790
12997
  if (!fp.name) return false;
12791
12998
  return candidates.some(
@@ -12807,7 +13014,7 @@ function matchRenamedElement(fp, candidates) {
12807
13014
  let best = null;
12808
13015
  let bestScore = -1;
12809
13016
  for (const c of pool) {
12810
- const s = textSimilarity(c.name, fp.name);
13017
+ const s = significantWordSimilarity(c.name, fp.name);
12811
13018
  if (s > bestScore) {
12812
13019
  bestScore = s;
12813
13020
  best = c;
@@ -12827,12 +13034,12 @@ function freshLocatorsFromCandidate(c) {
12827
13034
  if (!name) return out;
12828
13035
  out.push(
12829
13036
  c.level != null ? {
12830
- locator: `getByRole('${escapeQuote(c.role)}', { name: '${escapeQuote(name)}', level: ${c.level} })`,
13037
+ locator: `getByRole(${quoteJsString(c.role)}, { name: ${quoteJsString(name)}, level: ${c.level} })`,
12831
13038
  method: "getByRole",
12832
13039
  args: { role: c.role, name, level: c.level },
12833
13040
  score: ELEMENT_MATCH_SCORES.role
12834
13041
  } : {
12835
- locator: `getByRole('${escapeQuote(c.role)}', { name: '${escapeQuote(name)}' })`,
13042
+ locator: `getByRole(${quoteJsString(c.role)}, { name: ${quoteJsString(name)} })`,
12836
13043
  method: "getByRole",
12837
13044
  args: { role: c.role, name },
12838
13045
  score: ELEMENT_MATCH_SCORES.role
@@ -12840,14 +13047,14 @@ function freshLocatorsFromCandidate(c) {
12840
13047
  );
12841
13048
  if (TEXT_CONTENT_ROLES.has(c.role)) {
12842
13049
  out.push({
12843
- locator: `getByText('${escapeQuote(name)}')`,
13050
+ locator: `getByText(${quoteJsString(name)})`,
12844
13051
  method: "getByText",
12845
13052
  args: { text: name },
12846
13053
  score: ELEMENT_MATCH_SCORES.text
12847
13054
  });
12848
13055
  } else if (FORM_FIELD_ROLES.has(c.role)) {
12849
13056
  out.push({
12850
- locator: `getByLabel('${escapeQuote(name)}')`,
13057
+ locator: `getByLabel(${quoteJsString(name)})`,
12851
13058
  method: "getByLabel",
12852
13059
  args: { label: name },
12853
13060
  score: ELEMENT_MATCH_SCORES.label
@@ -12992,7 +13199,6 @@ var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
12992
13199
  "getByTitle",
12993
13200
  "getByAltText"
12994
13201
  ]);
12995
- var escAttr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
12996
13202
  var SUGG_TEXT_ROLES = /* @__PURE__ */ new Set([
12997
13203
  "button",
12998
13204
  "link",
@@ -13025,9 +13231,9 @@ function failedNameAndRole(failed) {
13025
13231
  function renderFailing(failed) {
13026
13232
  const { role, name } = failedNameAndRole(failed);
13027
13233
  if (failed.method === "getByRole") {
13028
- return name ? `getByRole('${escAttr(role ?? "")}', { name: '${escAttr(name)}' })` : `getByRole('${escAttr(role ?? "")}')`;
13234
+ return name ? `getByRole(${quoteJsString(role ?? "")}, { name: ${quoteJsString(name)} })` : `getByRole(${quoteJsString(role ?? "")})`;
13029
13235
  }
13030
- return `${failed.method}('${escAttr(name ?? "")}')`;
13236
+ return `${failed.method}(${quoteJsString(name ?? "")})`;
13031
13237
  }
13032
13238
  function freshSuggestions(candidate, failedMethod) {
13033
13239
  const out = [];
@@ -13037,9 +13243,9 @@ function freshSuggestions(candidate, failedMethod) {
13037
13243
  if (!out.includes(s)) out.push(s);
13038
13244
  };
13039
13245
  const levelPart = candidate.level != null ? `, level: ${candidate.level}` : "";
13040
- const roleLoc = `getByRole('${escAttr(role)}', { name: '${escAttr(name)}'${levelPart} })`;
13041
- const textLoc = `getByText('${escAttr(name)}')`;
13042
- const labelLoc = `getByLabel('${escAttr(name)}')`;
13246
+ const roleLoc = `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(name)}${levelPart} })`;
13247
+ const textLoc = `getByText(${quoteJsString(name)})`;
13248
+ const labelLoc = `getByLabel(${quoteJsString(name)})`;
13043
13249
  if (failedMethod === "getByText" && SUGG_TEXT_ROLES.has(role)) push(textLoc);
13044
13250
  if (failedMethod === "getByLabel" && SUGG_FIELD_ROLES.has(role)) push(labelLoc);
13045
13251
  push(roleLoc);
@@ -13102,10 +13308,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
13102
13308
  }
13103
13309
  let rel = file;
13104
13310
  try {
13105
- rel = path27.relative(process.cwd(), file);
13311
+ rel = path25.relative(process.cwd(), file);
13106
13312
  } catch {
13107
13313
  }
13108
- rel = rel.split(path27.sep).join("/");
13314
+ rel = rel.split(path25.sep).join("/");
13109
13315
  if (rel.startsWith("./")) rel = rel.slice(2);
13110
13316
  return `${rel}:${m[3]}:${m[4]}`;
13111
13317
  }
@@ -13335,8 +13541,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
13335
13541
  }
13336
13542
 
13337
13543
  // src/internal/capture/pick-on-failure.ts
13338
- var path28 = __toESM(require("path"));
13339
- var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
13544
+ var path26 = __toESM(require("path"));
13340
13545
  function parseLeafLocatorExpression(rawExpr) {
13341
13546
  const leaf = parseLeafLocatorCall(rawExpr);
13342
13547
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -13345,14 +13550,13 @@ function deriveFailedLocator(testInfo) {
13345
13550
  const info = testInfo;
13346
13551
  const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
13347
13552
  for (const err of errors) {
13348
- const text = `${err.message ?? ""}
13349
- ${err.stack ?? ""}`.replace(ANSI_RE2, "");
13350
- const line = /^\s*Locator:\s*(.+)$/m.exec(text);
13351
- if (!line) continue;
13352
- const parsed2 = parseLeafLocatorExpression(line[1].trim());
13553
+ const header = readLocatorHeader(stripAnsi(`${err.message ?? ""}
13554
+ ${err.stack ?? ""}`));
13555
+ if (!header) continue;
13556
+ const parsed2 = parseLeafLocatorExpression(header);
13353
13557
  if (!parsed2) continue;
13354
13558
  const loc = err.location;
13355
- const location = loc ? `${path28.relative(process.cwd(), loc.file).split(path28.sep).join("/")}:${loc.line}:${loc.column}` : null;
13559
+ const location = loc ? `${path26.relative(process.cwd(), loc.file).split(path26.sep).join("/")}:${loc.line}:${loc.column}` : null;
13356
13560
  return { method: parsed2.method, args: parsed2.args, location };
13357
13561
  }
13358
13562
  return null;
@@ -13505,7 +13709,7 @@ function applyPickToSnapshots(snapshots, pick) {
13505
13709
  }
13506
13710
 
13507
13711
  // src/internal/capture/pause-at.ts
13508
- var path29 = __toESM(require("path"));
13712
+ var path27 = __toESM(require("path"));
13509
13713
 
13510
13714
  // ../core/src/editor-send.ts
13511
13715
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
@@ -13549,9 +13753,9 @@ var PauseSet = class {
13549
13753
  var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
13550
13754
  function callSiteFile(file, cwd) {
13551
13755
  const slashed = file.replace(/\\/g, "/");
13552
- const absolute = path29.isAbsolute(file) || path29.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
13553
- let rel = absolute ? path29.relative(cwd, file) : path29.normalize(slashed);
13554
- rel = rel.split(path29.sep).join("/").replace(/\\/g, "/");
13756
+ const absolute = path27.isAbsolute(file) || path27.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
13757
+ let rel = absolute ? path27.relative(cwd, file) : path27.normalize(slashed);
13758
+ rel = rel.split(path27.sep).join("/").replace(/\\/g, "/");
13555
13759
  if (rel.startsWith("./")) rel = rel.slice(2);
13556
13760
  return rel;
13557
13761
  }
@@ -14035,7 +14239,7 @@ function configuredCodeReachRoots() {
14035
14239
  }
14036
14240
  } catch {
14037
14241
  }
14038
- return raw.split(path30.delimiter).filter(Boolean);
14242
+ return raw.split(path28.delimiter).filter(Boolean);
14039
14243
  }
14040
14244
  async function stopSinkCodeReach(sink, closing) {
14041
14245
  const stopping = [];
@@ -14051,9 +14255,7 @@ async function stopSinkCodeReach(sink, closing) {
14051
14255
  }
14052
14256
  if (stopping.length > 0) {
14053
14257
  const info = sink.testInfo;
14054
- const configFile = info?.config.configFile;
14055
- const configDir = configFile ? path30.dirname(configFile) : info?.config.rootDir ?? process.cwd();
14056
- const roots = codeReachRoots(configDir, configuredCodeReachRoots());
14258
+ const roots = codeReachRoots(configDirFromConfig(info?.config), configuredCodeReachRoots());
14057
14259
  for (const [page, session] of stopping) {
14058
14260
  session.stopping = stopPageCodeReach(sink, page, session, roots);
14059
14261
  underway.push(session.stopping);
@@ -14195,8 +14397,8 @@ function buildPageState(raw, cookies, interaction) {
14195
14397
  length: typeof e.length === "number" ? e.length : 0
14196
14398
  }));
14197
14399
  return {
14198
- url: raw.url,
14199
- hash: raw.hash || null,
14400
+ url: raw.url.split("#")[0],
14401
+ hash: raw.hash ? maskUrlFragment(raw.hash) : null,
14200
14402
  historyState: historyState || null,
14201
14403
  localStorage: capStorage(raw.localStorage),
14202
14404
  sessionStorage: capStorage(raw.sessionStorage),
@@ -15213,6 +15415,7 @@ var piwiFixtures = {
15213
15415
  const plan = loadFlakePlan();
15214
15416
  const role = flakeRoleForTest(plan, {
15215
15417
  file: testInfo.file,
15418
+ base: configDirFromConfig(testInfo.config),
15216
15419
  title: testInfo.title,
15217
15420
  titlePath: testInfo.titlePath,
15218
15421
  project: testInfo.project.name
@@ -15226,7 +15429,8 @@ var piwiFixtures = {
15226
15429
  sink.probeItem = probeItemForTest({
15227
15430
  title: testInfo.title,
15228
15431
  file: testInfo.file,
15229
- titlePath: testInfo.titlePath
15432
+ titlePath: testInfo.titlePath,
15433
+ base: configDirFromConfig(testInfo.config)
15230
15434
  });
15231
15435
  activeResourceLedger()?.testStarted(testInfo);
15232
15436
  currentSink = sink;
@@ -15270,6 +15474,7 @@ async function recordFlakeAttempt(flake, testInfo) {
15270
15474
  flakeResultLine(flake.plan, {
15271
15475
  role: flake.role,
15272
15476
  file: testInfo.file,
15477
+ base: configDirFromConfig(testInfo.config),
15273
15478
  title: testInfo.title,
15274
15479
  project: testInfo.project.name || null,
15275
15480
  browserName: flake.browserName,
@@ -15291,12 +15496,12 @@ function extendPiwiFixtures(test) {
15291
15496
 
15292
15497
  // src/internal/ai/ai-fixtures.ts
15293
15498
  var fs26 = __toESM(require("fs"));
15294
- var path33 = __toESM(require("path"));
15499
+ var path31 = __toESM(require("path"));
15295
15500
  var import_test = require("@playwright/test");
15296
15501
 
15297
15502
  // src/internal/ai/artifact.ts
15298
15503
  var fs25 = __toESM(require("fs"));
15299
- var path31 = __toESM(require("path"));
15504
+ var path29 = __toESM(require("path"));
15300
15505
  var ARTIFACT_VERSION = 1;
15301
15506
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
15302
15507
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -15400,7 +15605,7 @@ function writeEntry(file, entry) {
15400
15605
  const canonical = serializeEntry(entry);
15401
15606
  return withEntryLock(file, () => {
15402
15607
  if (fs25.existsSync(file) && fs25.readFileSync(file, "utf8") === canonical) return { written: false };
15403
- fs25.mkdirSync(path31.dirname(file), { recursive: true });
15608
+ fs25.mkdirSync(path29.dirname(file), { recursive: true });
15404
15609
  const tmp = `${file}.${process.pid}.tmp`;
15405
15610
  fs25.writeFileSync(tmp, canonical);
15406
15611
  fs25.renameSync(tmp, file);
@@ -15409,7 +15614,7 @@ function writeEntry(file, entry) {
15409
15614
  }
15410
15615
  function withEntryLock(file, fn) {
15411
15616
  const lock = `${file}.lock`;
15412
- fs25.mkdirSync(path31.dirname(file), { recursive: true });
15617
+ fs25.mkdirSync(path29.dirname(file), { recursive: true });
15413
15618
  const deadline = Date.now() + 5e3;
15414
15619
  for (; ; ) {
15415
15620
  try {
@@ -15437,28 +15642,28 @@ function withEntryLock(file, fn) {
15437
15642
  }
15438
15643
 
15439
15644
  // src/internal/ai/keys.ts
15440
- var crypto6 = __toESM(require("crypto"));
15441
- var path32 = __toESM(require("path"));
15645
+ var crypto7 = __toESM(require("crypto"));
15646
+ var path30 = __toESM(require("path"));
15442
15647
  var DEFAULT_AI_DIR = "__piwi__";
15443
15648
  function normalizeTemplate(template) {
15444
15649
  return template.trim().replace(/\s+/g, " ").toLowerCase();
15445
15650
  }
15446
15651
  function hashTemplate(normalized, ordinal = 0) {
15447
15652
  const input = ordinal > 0 ? `${normalized}#${ordinal}` : normalized;
15448
- return crypto6.createHash("sha256").update(input).digest("hex").slice(0, 8);
15653
+ return crypto7.createHash("sha256").update(input).digest("hex").slice(0, 8);
15449
15654
  }
15450
15655
  function slug(text, maxLength = 40) {
15451
15656
  const base = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
15452
15657
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
15453
15658
  }
15454
15659
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
15455
- return path32.join(path32.dirname(specFile), dir, path32.basename(specFile));
15660
+ return path30.join(path30.dirname(specFile), dir, path30.basename(specFile));
15456
15661
  }
15457
15662
  function entryPath(params) {
15458
15663
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
15459
15664
  const hash = hashTemplate(basis, params.ordinal ?? 0);
15460
15665
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
15461
- return path32.join(entryDir(params.specFile, params.dir), name);
15666
+ return path30.join(entryDir(params.specFile, params.dir), name);
15462
15667
  }
15463
15668
  function parseLocation2(location) {
15464
15669
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -15865,6 +16070,8 @@ async function groundElement(page, element, params) {
15865
16070
  // src/internal/ai/resolution.ts
15866
16071
  var MAX_SNAPSHOT_CHARS = 24e3;
15867
16072
  var DEFAULT_MAX_STEPS = 20;
16073
+ var MAX_OBSERVED_RESPONSES = 50;
16074
+ var MAX_OBSERVED_URL_CHARS = 4e3;
15868
16075
  var ServerStepResolver = class {
15869
16076
  constructor(serverUrl, apiKey) {
15870
16077
  this.serverUrl = serverUrl;
@@ -16016,14 +16223,15 @@ async function resolveRun(template, ctx) {
16016
16223
  ctx.responseWaitTimeout
16017
16224
  );
16018
16225
  history.push({ action: decision.action, element: decision.element, value: decision.value });
16019
- if (observed.length > 0) {
16226
+ const observedResponses = [...new Set(observed.map((url) => maskValues(url, ctx.params)))].filter((url) => url.length <= MAX_OBSERVED_URL_CHARS).slice(0, MAX_OBSERVED_RESPONSES);
16227
+ if (observedResponses.length > 0) {
16020
16228
  const waitDecision = await ctx.resolver.resolveStep({
16021
16229
  kind: "wait",
16022
16230
  template,
16023
16231
  paramNames,
16024
16232
  ariaSnapshot: "",
16025
16233
  history,
16026
- observedResponses: observed.map((url) => maskValues(url, ctx.params))
16234
+ observedResponses
16027
16235
  });
16028
16236
  if (waitDecision.waitForResponse) step.waitForResponse = waitDecision.waitForResponse;
16029
16237
  }
@@ -16058,7 +16266,7 @@ function testIdentity(testInfo) {
16058
16266
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
16059
16267
  }
16060
16268
  function relativeToCwd(file) {
16061
- return path33.relative(process.cwd(), file).split(path33.sep).join("/");
16269
+ return path31.relative(process.cwd(), file).split(path31.sep).join("/");
16062
16270
  }
16063
16271
  function recordIntents(intents, entry) {
16064
16272
  const add = (locator, kind) => {