@piwitests/reporter 0.55.0 → 0.56.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;
@@ -4771,14 +4869,14 @@ var ServerCoverageCollector = class {
4771
4869
  // src/public/global-setup.ts
4772
4870
  function createGlobalSetup(options, userSetup) {
4773
4871
  return async function globalSetupFn(config) {
4774
- const piwiReporterPath = path13.resolve(__dirname, "./index.js");
4872
+ const piwiReporterPath = path14.resolve(__dirname, "./index.js");
4775
4873
  let inlineReporterOptions = {};
4776
4874
  if (Array.isArray(config?.reporter)) {
4777
4875
  for (const r of config.reporter) {
4778
4876
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
4779
4877
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
4780
4878
  try {
4781
- return path13.resolve(require.resolve(r[0])) === piwiReporterPath;
4879
+ return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
4782
4880
  } catch {
4783
4881
  return false;
4784
4882
  }
@@ -4814,7 +4912,7 @@ function createGlobalSetup(options, userSetup) {
4814
4912
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
4815
4913
  if (r[0].toLowerCase().includes("piwi")) return true;
4816
4914
  try {
4817
- return path13.resolve(require.resolve(r[0])) === piwiReporterPath;
4915
+ return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
4818
4916
  } catch {
4819
4917
  return false;
4820
4918
  }
@@ -4898,12 +4996,11 @@ function createGlobalSetup(options, userSetup) {
4898
4996
  }
4899
4997
 
4900
4998
  // src/public/config-wrapper.ts
4901
- var path16 = __toESM(require("path"));
4999
+ var path15 = __toESM(require("path"));
4902
5000
  var fs17 = __toESM(require("fs"));
4903
5001
 
4904
5002
  // src/internal/probe/mode.ts
4905
5003
  var fs15 = __toESM(require("fs"));
4906
- var path14 = __toESM(require("path"));
4907
5004
 
4908
5005
  // src/internal/probe/plan.ts
4909
5006
  function probeRunMetadata(base = {}) {
@@ -4915,8 +5012,8 @@ function parseRouteKey(routeKey2) {
4915
5012
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
4916
5013
  }
4917
5014
  function patternToRegex(pattern) {
4918
- const path34 = pattern.split("?")[0] ?? pattern;
4919
- const escaped = path34.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
5015
+ const path32 = pattern.split("?")[0] ?? pattern;
5016
+ const escaped = path32.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
4920
5017
  return new RegExp(`^${escaped}$`);
4921
5018
  }
4922
5019
  function suitePathEqual(a, b) {
@@ -4939,13 +5036,13 @@ function matchProbeItem(plan, test) {
4939
5036
  function requestMatchesRoute(method, url, routeKey2) {
4940
5037
  const { method: rm, pattern } = parseRouteKey(routeKey2);
4941
5038
  if (rm && rm !== method.toUpperCase()) return false;
4942
- let path34;
5039
+ let path32;
4943
5040
  try {
4944
- path34 = new URL(url).pathname;
5041
+ path32 = new URL(url).pathname;
4945
5042
  } catch {
4946
- path34 = url.split("?")[0] ?? url;
5043
+ path32 = url.split("?")[0] ?? url;
4947
5044
  }
4948
- return patternToRegex(pattern).test(path34);
5045
+ return patternToRegex(pattern).test(path32);
4949
5046
  }
4950
5047
  function createProbeState() {
4951
5048
  return { navigated: false, matches: 0 };
@@ -4967,10 +5064,10 @@ function isProbeMode() {
4967
5064
  function loadPlan() {
4968
5065
  if (planCache !== void 0) return planCache;
4969
5066
  planCache = null;
4970
- const path34 = process.env[PIWI_PROBE_ENV.plan];
4971
- if (path34) {
5067
+ const path32 = process.env[PIWI_PROBE_ENV.plan];
5068
+ if (path32) {
4972
5069
  try {
4973
- planCache = JSON.parse(fs15.readFileSync(path34, "utf8"));
5070
+ planCache = JSON.parse(fs15.readFileSync(path32, "utf8"));
4974
5071
  } catch {
4975
5072
  planCache = null;
4976
5073
  }
@@ -4980,7 +5077,7 @@ function loadPlan() {
4980
5077
  function probeItemForTest(test) {
4981
5078
  const plan = loadPlan();
4982
5079
  if (!plan) return null;
4983
- const filePath2 = test.file ? path14.relative(process.cwd(), test.file).split(path14.sep).join("/") : null;
5080
+ const filePath2 = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
4984
5081
  const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
4985
5082
  return matchProbeItem(plan, { title: test.title, filePath: filePath2, suitePath });
4986
5083
  }
@@ -4994,10 +5091,10 @@ function outcomeFromStatus(status, applied) {
4994
5091
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
4995
5092
  }
4996
5093
  function recordProbeOutcome(line) {
4997
- const path34 = process.env[PIWI_PROBE_ENV.results];
4998
- if (!path34) return;
5094
+ const path32 = process.env[PIWI_PROBE_ENV.results];
5095
+ if (!path32) return;
4999
5096
  try {
5000
- fs15.appendFileSync(path34, `${JSON.stringify(line)}
5097
+ fs15.appendFileSync(path32, `${JSON.stringify(line)}
5001
5098
  `);
5002
5099
  } catch {
5003
5100
  }
@@ -5005,7 +5102,6 @@ function recordProbeOutcome(line) {
5005
5102
 
5006
5103
  // src/internal/flake/mode.ts
5007
5104
  var fs16 = __toESM(require("fs"));
5008
- var path15 = __toESM(require("path"));
5009
5105
 
5010
5106
  // ../core/src/error-signature.ts
5011
5107
  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 +5621,12 @@ function flakeRunMetadata(stamp, base = {}) {
5525
5621
  [RUN_ORIGIN_METADATA_KEY]: ref === void 0 ? { kind: "flake-lab" } : { kind: "flake-lab", ref }
5526
5622
  };
5527
5623
  }
5528
- function relativeTestFile2(file) {
5529
- return file ? path15.relative(process.cwd(), file).split(path15.sep).join("/") : null;
5624
+ function relativeTestFile(file, base = process.cwd()) {
5625
+ return file ? relativeTestPath(file, base) : null;
5530
5626
  }
5531
5627
  function flakeRoleForTest(plan, test) {
5532
5628
  const running = {
5533
- file: relativeTestFile2(test.file),
5629
+ file: relativeTestFile(test.file, test.base),
5534
5630
  title: test.title,
5535
5631
  suite: test.titlePath ? test.titlePath.slice(1, -1) : void 0
5536
5632
  };
@@ -5646,7 +5742,7 @@ function flakeResultLine(plan, attempt) {
5646
5742
  experimentId: plan.experimentId,
5647
5743
  armId: plan.arm.id,
5648
5744
  role: attempt.role,
5649
- file: relativeTestFile2(attempt.file),
5745
+ file: relativeTestFile(attempt.file, attempt.base),
5650
5746
  title: attempt.title,
5651
5747
  project: attempt.project,
5652
5748
  browserName: attempt.browserName,
@@ -5737,9 +5833,9 @@ function injectReporter(reporter, piwiOptions) {
5737
5833
  }
5738
5834
  function resolveSetupModule() {
5739
5835
  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")
5836
+ path15.join(__dirname, "global-setup-module.js"),
5837
+ path15.join(__dirname, "..", "global-setup-module.js"),
5838
+ path15.join(__dirname, "..", "global-setup-module.ts")
5743
5839
  ];
5744
5840
  return candidates.find((candidate) => fs17.existsSync(candidate)) ?? candidates[0];
5745
5841
  }
@@ -5949,9 +6045,6 @@ function collectTestMetadata(annotations) {
5949
6045
  return parseTestMetadata(annotations);
5950
6046
  }
5951
6047
 
5952
- // src/internal/collect/error-text.ts
5953
- var path17 = __toESM(require("path"));
5954
-
5955
6048
  // ../core/src/error-text.ts
5956
6049
  function joinErrorMessages(errors) {
5957
6050
  if (!errors || errors.length === 0) return null;
@@ -5968,24 +6061,26 @@ function joinErrorMessages(errors) {
5968
6061
  return parts.join("\n---\n") || null;
5969
6062
  }
5970
6063
  function appendErrorLocation(text, location) {
5971
- if (!location?.file || /\n\s+at\s/.test(text)) return text;
6064
+ if (!location?.file || /\n[ \t]+at\s/.test(text)) return text;
5972
6065
  return `${text}
5973
6066
  at ${location.file}:${location.line}:${location.column}`;
5974
6067
  }
5975
6068
 
5976
6069
  // src/internal/collect/error-text.ts
5977
- function buildErrorText(result) {
6070
+ function buildErrorText(result, base = process.cwd()) {
5978
6071
  const text = joinErrorMessages(result.errors);
5979
6072
  if (!text) return null;
5980
6073
  const loc = result.error?.location;
5981
6074
  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 });
6075
+ return appendErrorLocation(text, { file: relativeTestPath(loc.file, base), line: loc.line, column: loc.column });
5984
6076
  }
5985
6077
 
6078
+ // src/internal/submit/run-submitter.ts
6079
+ var import_node_crypto2 = require("crypto");
6080
+
5986
6081
  // src/internal/support/ci-output.ts
5987
6082
  var fs18 = __toESM(require("fs"));
5988
- var path18 = __toESM(require("path"));
6083
+ var path16 = __toESM(require("path"));
5989
6084
  var SUMMARY_MAX_FAILURES = 20;
5990
6085
  function emitRunOutputs(output, logger, outputFile, env = process.env) {
5991
6086
  logger.info(`View run: ${output.runUrl}`);
@@ -5995,7 +6090,7 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
5995
6090
  }
5996
6091
  function writeOutputFile(file, output, logger) {
5997
6092
  try {
5998
- const dir = path18.dirname(file);
6093
+ const dir = path16.dirname(file);
5999
6094
  if (dir && dir !== ".") fs18.mkdirSync(dir, { recursive: true });
6000
6095
  fs18.writeFileSync(
6001
6096
  file,
@@ -6107,8 +6202,8 @@ function ciBuildUrlFromMetadata(metadata) {
6107
6202
 
6108
6203
  // src/internal/submit/run-submitter.ts
6109
6204
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
6110
- function markSubmitFallback(run, path34, reason) {
6111
- run.metadata.ingestHealth = { submitFallback: { path: path34, reason } };
6205
+ function markSubmitFallback(run, path32, reason) {
6206
+ run.metadata.ingestHealth = { submitFallback: { path: path32, reason } };
6112
6207
  }
6113
6208
  var RunSubmitter = class {
6114
6209
  /**
@@ -6126,6 +6221,8 @@ var RunSubmitter = class {
6126
6221
  this.streamManager = streamManager;
6127
6222
  this.logger = logger;
6128
6223
  this.failureLinks = failureLinks;
6224
+ /** The key every batch delivery of this run carries, so the dashboard stores the run once. */
6225
+ this.deliveryKey = (0, import_node_crypto2.randomUUID)();
6129
6226
  }
6130
6227
  /**
6131
6228
  * Run the fallback ladder for a completed test run, within the
@@ -6181,8 +6278,9 @@ var RunSubmitter = class {
6181
6278
  auth = sm?.auth ?? await this.httpClient.resolveAuth(run.options);
6182
6279
  } catch (error) {
6183
6280
  this.logger.error(`Authentication failed: ${errorMessage(error)}`);
6281
+ this.logRecoverySaved();
6184
6282
  this.saveRecovery(this.buildRunPayload(run, overallStatus, duration));
6185
- throw error;
6283
+ return null;
6186
6284
  }
6187
6285
  if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
6188
6286
  let outcome = { done: false, output: null };
@@ -6256,7 +6354,8 @@ var RunSubmitter = class {
6256
6354
  shardTotal: run.shardInfo?.total,
6257
6355
  isFullRun: run.isFullRun,
6258
6356
  filterDetails: run.filterDetails,
6259
- resourceReport: run.resourceReport ?? null
6357
+ resourceReport: run.resourceReport ?? null,
6358
+ deliveryKey: this.deliveryKey
6260
6359
  };
6261
6360
  }
6262
6361
  async tryFinishStreaming(run, overallStatus, duration, auth) {
@@ -6322,11 +6421,7 @@ var RunSubmitter = class {
6322
6421
  this.dropLocalCopies();
6323
6422
  return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
6324
6423
  } 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
- }
6424
+ if (error instanceof HttpError && error.status === 401 && !auth) return this.stopForMissingAuth(run, payload);
6330
6425
  this.logger.warn(`Failed to upload with files: ${errorMessage(error)}`);
6331
6426
  this.logger.info("Falling back to JSON upload...");
6332
6427
  return { done: false, output: null };
@@ -6339,22 +6434,20 @@ var RunSubmitter = class {
6339
6434
  this.dropLocalCopies();
6340
6435
  return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
6341
6436
  } 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
- }
6437
+ if (error instanceof HttpError && error.status === 401 && !auth) return this.stopForMissingAuth(run, payload);
6347
6438
  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.`
6439
+ this.logRecoverySaved(
6440
+ `If this keeps happening, check that serverUrl (${run.options.serverUrl ?? "not set"}) is correct and reachable.`
6350
6441
  );
6351
6442
  this.saveRecovery(payload);
6352
6443
  return { done: true, output: null };
6353
6444
  }
6354
6445
  }
6355
- /** The run reached the server: drop the recovery copy and the run's buffered live events. */
6446
+ /**
6447
+ * The run reached the server: drop its buffered live events. A recovery file
6448
+ * on disk is an earlier run's, deleted only once its own replay succeeds.
6449
+ */
6356
6450
  dropLocalCopies() {
6357
- this.recovery.clear();
6358
6451
  this.streamManager?.discardBuffered();
6359
6452
  }
6360
6453
  /**
@@ -6364,22 +6457,35 @@ var RunSubmitter = class {
6364
6457
  saveRecovery(payload) {
6365
6458
  this.recovery.save(serializeRun(payload, { includeTestCases: true }));
6366
6459
  }
6367
- /** Log one actionable line explaining how to fix a 401 caused by a missing credential. */
6368
- logAuthRequired(serverUrl) {
6460
+ /**
6461
+ * The dashboard asked for a credential the reporter has none of: no other
6462
+ * rung can succeed, so the ladder stops with the run saved for the next one.
6463
+ * The test run's own result stays what Playwright made it.
6464
+ */
6465
+ stopForMissingAuth(run, payload) {
6369
6466
  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.`
6467
+ `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.`
6468
+ );
6469
+ this.logRecoverySaved();
6470
+ this.saveRecovery(payload);
6471
+ return { done: true, output: null };
6472
+ }
6473
+ /** Say where the run went when the dashboard did not take it, with an optional hint. */
6474
+ logRecoverySaved(hint) {
6475
+ this.logger.info(
6476
+ `Saved a local recovery copy \u2014 it will be uploaded automatically on your next test run.${hint ? ` ${hint}` : ""}`
6371
6477
  );
6372
6478
  }
6373
6479
  };
6374
6480
 
6375
6481
  // src/internal/capture/resource-ledger.ts
6376
6482
  var fs20 = __toESM(require("fs"));
6377
- var path21 = __toESM(require("path"));
6483
+ var path19 = __toESM(require("path"));
6378
6484
  var import_node_module2 = require("module");
6379
6485
 
6380
6486
  // src/internal/capture/quiet-capture.ts
6381
6487
  var import_node_module = require("module");
6382
- var path19 = __toESM(require("path"));
6488
+ var path17 = __toESM(require("path"));
6383
6489
  var BOXED_PREFIX_EXPORTS = [
6384
6490
  [
6385
6491
  "playwright-core/lib/coreBundle",
@@ -6416,7 +6522,7 @@ function resolveBoxing() {
6416
6522
  if (typeof set !== "function") continue;
6417
6523
  return {
6418
6524
  set,
6419
- prefixes: [path19.dirname(playwrightPackageJson), path19.join(root, "dist") + path19.sep]
6525
+ prefixes: [path17.dirname(playwrightPackageJson), path17.join(root, "dist") + path17.sep]
6420
6526
  };
6421
6527
  }
6422
6528
  } catch {
@@ -6449,7 +6555,7 @@ var import_node_perf_hooks = require("perf_hooks");
6449
6555
 
6450
6556
  // src/internal/support/system-readers.ts
6451
6557
  var fs19 = __toESM(require("fs"));
6452
- var path20 = __toESM(require("path"));
6558
+ var path18 = __toESM(require("path"));
6453
6559
  var CLK_TCK = 100;
6454
6560
  function readText(file) {
6455
6561
  try {
@@ -6587,7 +6693,7 @@ var ProcFs = class {
6587
6693
  }
6588
6694
  /** Whether this looks like a Linux `/proc` at all. */
6589
6695
  available() {
6590
- return fs19.existsSync(path20.join(this.root, "stat"));
6696
+ return fs19.existsSync(path18.join(this.root, "stat"));
6591
6697
  }
6592
6698
  /** Every process, keyed by pid. */
6593
6699
  table() {
@@ -6600,32 +6706,32 @@ var ProcFs = class {
6600
6706
  }
6601
6707
  for (const name of names) {
6602
6708
  if (!/^\d+$/.test(name)) continue;
6603
- const text = readText(path20.join(this.root, name, "stat"));
6709
+ const text = readText(path18.join(this.root, name, "stat"));
6604
6710
  const stat = text ? parseProcStat(Number(name), text) : null;
6605
6711
  if (stat) out.set(stat.pid, stat);
6606
6712
  }
6607
6713
  return out;
6608
6714
  }
6609
6715
  stat(pid) {
6610
- const text = readText(path20.join(this.root, String(pid), "stat"));
6716
+ const text = readText(path18.join(this.root, String(pid), "stat"));
6611
6717
  return text ? parseProcStat(pid, text) : null;
6612
6718
  }
6613
6719
  runWaitMs(pid) {
6614
- const text = readText(path20.join(this.root, String(pid), "schedstat"));
6720
+ const text = readText(path18.join(this.root, String(pid), "schedstat"));
6615
6721
  return text ? parseSchedstat(text) : null;
6616
6722
  }
6617
6723
  cmdline(pid) {
6618
- return (readText(path20.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
6724
+ return (readText(path18.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
6619
6725
  }
6620
6726
  /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
6621
6727
  statusKb(pid, key) {
6622
- const text = readText(path20.join(this.root, String(pid), "status"));
6728
+ const text = readText(path18.join(this.root, String(pid), "status"));
6623
6729
  return text ? parseKbField(text, key) : null;
6624
6730
  }
6625
6731
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
6626
6732
  async pssKb(pid) {
6627
6733
  try {
6628
- return parseKbField(await fs19.promises.readFile(path20.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
6734
+ return parseKbField(await fs19.promises.readFile(path18.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
6629
6735
  } catch {
6630
6736
  return null;
6631
6737
  }
@@ -6633,29 +6739,29 @@ var ProcFs = class {
6633
6739
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
6634
6740
  resetPeakRss(pid) {
6635
6741
  try {
6636
- fs19.writeFileSync(path20.join(this.root, String(pid), "clear_refs"), "5");
6742
+ fs19.writeFileSync(path18.join(this.root, String(pid), "clear_refs"), "5");
6637
6743
  return true;
6638
6744
  } catch {
6639
6745
  return false;
6640
6746
  }
6641
6747
  }
6642
6748
  cpuTicks() {
6643
- const text = readText(path20.join(this.root, "stat"));
6749
+ const text = readText(path18.join(this.root, "stat"));
6644
6750
  return text ? parseCpuTicks(text) : null;
6645
6751
  }
6646
6752
  /** Total stall time of a resource's pressure file, in µs. */
6647
6753
  pressureUs(resource) {
6648
- const text = readText(path20.join(this.root, "pressure", resource));
6754
+ const text = readText(path18.join(this.root, "pressure", resource));
6649
6755
  return text ? parsePsiTotal(text) : null;
6650
6756
  }
6651
6757
  memAvailableKb() {
6652
- const text = readText(path20.join(this.root, "meminfo"));
6758
+ const text = readText(path18.join(this.root, "meminfo"));
6653
6759
  return text ? parseKbField(text, "MemAvailable") : null;
6654
6760
  }
6655
6761
  /** Open file descriptors of a process. */
6656
6762
  fdCount(pid) {
6657
6763
  try {
6658
- return fs19.readdirSync(path20.join(this.root, String(pid), "fd")).length;
6764
+ return fs19.readdirSync(path18.join(this.root, String(pid), "fd")).length;
6659
6765
  } catch {
6660
6766
  return null;
6661
6767
  }
@@ -6676,7 +6782,7 @@ var CgroupFs = class {
6676
6782
  }
6677
6783
  /** The directory of a controller (v1) or of the unified hierarchy (v2), when it exists. */
6678
6784
  dirs() {
6679
- const text = readText(path20.join(this.procRoot, String(this.pid), "cgroup"));
6785
+ const text = readText(path18.join(this.procRoot, String(this.pid), "cgroup"));
6680
6786
  if (!text) return null;
6681
6787
  const v1 = /* @__PURE__ */ new Map();
6682
6788
  let v2 = null;
@@ -6687,21 +6793,21 @@ var CgroupFs = class {
6687
6793
  else for (const controller of match[1].split(",")) v1.set(controller, match[2]);
6688
6794
  }
6689
6795
  const pick = (mount, sub) => {
6690
- const nested = path20.join(mount, sub);
6796
+ const nested = path18.join(mount, sub);
6691
6797
  if (fs19.existsSync(nested)) return nested;
6692
6798
  return fs19.existsSync(mount) ? mount : null;
6693
6799
  };
6694
6800
  if (v1.has("memory") || v1.has("cpu")) {
6695
6801
  const memoryPath = v1.get("memory");
6696
6802
  const cpuPath = v1.get("cpu");
6697
- const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path20.join(this.cgroupRoot, name)).find((dir) => fs19.existsSync(dir));
6803
+ const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path18.join(this.cgroupRoot, name)).find((dir) => fs19.existsSync(dir));
6698
6804
  return {
6699
6805
  version: 1,
6700
- memory: memoryPath !== void 0 ? pick(path20.join(this.cgroupRoot, "memory"), memoryPath) : null,
6806
+ memory: memoryPath !== void 0 ? pick(path18.join(this.cgroupRoot, "memory"), memoryPath) : null,
6701
6807
  cpu: cpuPath !== void 0 && cpuMount ? pick(cpuMount, cpuPath) : null
6702
6808
  };
6703
6809
  }
6704
- if (v2 !== null && fs19.existsSync(path20.join(this.cgroupRoot, "cgroup.controllers"))) {
6810
+ if (v2 !== null && fs19.existsSync(path18.join(this.cgroupRoot, "cgroup.controllers"))) {
6705
6811
  const dir = pick(this.cgroupRoot, v2);
6706
6812
  return { version: 2, memory: dir, cpu: dir };
6707
6813
  }
@@ -6710,7 +6816,7 @@ var CgroupFs = class {
6710
6816
  read() {
6711
6817
  const dirs = this.dirs();
6712
6818
  if (!dirs) return null;
6713
- const file = (dir, name) => dir ? readText(path20.join(dir, name)) : null;
6819
+ const file = (dir, name) => dir ? readText(path18.join(dir, name)) : null;
6714
6820
  if (dirs.version === 2) {
6715
6821
  const max = file(dirs.memory, "memory.max");
6716
6822
  const cpuMax = file(dirs.cpu, "cpu.max")?.trim().split(/\s+/);
@@ -6896,10 +7002,10 @@ var PAGE_READ_TIMEOUT_MS = 1e3;
6896
7002
  var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
6897
7003
  var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
6898
7004
  function relativePath(file) {
6899
- return path21.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path21.sep).join("/");
7005
+ return path19.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path19.sep).join("/");
6900
7006
  }
6901
7007
  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;
7008
+ return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path19.sep}node_modules${path19.sep}`) || file.includes("/node_modules/") || file === __filename;
6903
7009
  }
6904
7010
  function userSite(stack) {
6905
7011
  for (const line of (stack ?? "").split("\n").slice(1)) {
@@ -6947,7 +7053,7 @@ function playwrightTestInfoSource() {
6947
7053
  try {
6948
7054
  const packageJson = resolvePlaywrightPackageJson();
6949
7055
  if (!packageJson) return () => null;
6950
- const globals = (0, import_node_module2.createRequire)(packageJson)(path21.join(path21.dirname(packageJson), "lib", "globals.js"));
7056
+ const globals = (0, import_node_module2.createRequire)(packageJson)(path19.join(path19.dirname(packageJson), "lib", "globals.js"));
6951
7057
  const read = globals.currentTestInfo;
6952
7058
  if (typeof read !== "function") return () => null;
6953
7059
  return () => {
@@ -7436,7 +7542,7 @@ async function recordResourceCensus(testInfo) {
7436
7542
  }
7437
7543
 
7438
7544
  // src/internal/collect/resource-verdicts.ts
7439
- var path22 = __toESM(require("path"));
7545
+ var path20 = __toESM(require("path"));
7440
7546
  var OPEN_STEP_TITLES = {
7441
7547
  "Create context": "context",
7442
7548
  "Launch persistent context": "context",
@@ -7468,7 +7574,7 @@ function userFixturesOf(steps) {
7468
7574
  const walk = (list) => {
7469
7575
  for (const step of list) {
7470
7576
  const file = step.location?.file;
7471
- if (step.category === "fixture" && file && !file.includes(`${path22.sep}node_modules${path22.sep}`)) {
7577
+ if (step.category === "fixture" && file && !file.includes(`${path20.sep}node_modules${path20.sep}`)) {
7472
7578
  const name = step.title.replace(/^(?:Fixture\s+|fixture:\s*)/, "").replace(/^"(.*)"$/, "$1");
7473
7579
  if (!name.startsWith("piwi")) out.push(`${name} (${relativeFile(file)}:${step.location.line})`);
7474
7580
  }
@@ -7501,7 +7607,7 @@ function tallyLifecycleSteps(file, steps) {
7501
7607
  return tally;
7502
7608
  }
7503
7609
  function relativeFile(file) {
7504
- return path22.relative(process.cwd(), file).split(path22.sep).join("/");
7610
+ return path20.relative(process.cwd(), file).split(path20.sep).join("/");
7505
7611
  }
7506
7612
  function buildResourceReport(input) {
7507
7613
  const findings = [];
@@ -7861,7 +7967,7 @@ function formatResourceSummary(report, leakCheck, maxLines = 10) {
7861
7967
  // src/internal/collect/process-sampler.ts
7862
7968
  var fs21 = __toESM(require("fs"));
7863
7969
  var os8 = __toESM(require("os"));
7864
- var path23 = __toESM(require("path"));
7970
+ var path21 = __toESM(require("path"));
7865
7971
  var import_node_child_process2 = require("child_process");
7866
7972
  var WALK_FILE_CAP = 1e4;
7867
7973
  var SERIES_CAP = 3600;
@@ -7926,13 +8032,13 @@ function defaultWindowsQuery() {
7926
8032
  });
7927
8033
  }
7928
8034
  function freeBytes(dir) {
7929
- let current = path23.resolve(dir);
8035
+ let current = path21.resolve(dir);
7930
8036
  for (; ; ) {
7931
8037
  try {
7932
8038
  const stats = fs21.statfsSync(current);
7933
8039
  return stats.bavail * stats.bsize;
7934
8040
  } catch {
7935
- const parent = path23.dirname(current);
8041
+ const parent = path21.dirname(current);
7936
8042
  if (parent === current) return null;
7937
8043
  current = parent;
7938
8044
  }
@@ -7948,7 +8054,7 @@ async function dirBytes(dir, budget) {
7948
8054
  }
7949
8055
  for (const entry of entries) {
7950
8056
  if (budget.files <= 0) return bytes;
7951
- const full = path23.join(dir, entry.name);
8057
+ const full = path21.join(dir, entry.name);
7952
8058
  if (entry.isDirectory()) bytes += await dirBytes(full, budget);
7953
8059
  else if (entry.isFile()) {
7954
8060
  budget.files--;
@@ -7993,7 +8099,7 @@ var RunSampler = class {
7993
8099
  this.platform = options.platform ?? process.platform;
7994
8100
  this.proc = new ProcFs(options.procRoot);
7995
8101
  this.cgroup = new CgroupFs(options.procRoot, options.cgroupRoot);
7996
- this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path23.resolve(dir)))];
8102
+ this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path21.resolve(dir)))];
7997
8103
  this.tmpDir = options.tmpDir ?? os8.tmpdir();
7998
8104
  this.intervalMs = options.intervalMs ?? 1e3;
7999
8105
  this.heavyEvery = Math.max(1, options.heavyEvery ?? 5);
@@ -8195,7 +8301,7 @@ var RunSampler = class {
8195
8301
  } catch {
8196
8302
  return;
8197
8303
  }
8198
- this.noteTree(list, (proc) => roleOf(path23.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
8304
+ this.noteTree(list, (proc) => roleOf(path21.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
8199
8305
  }
8200
8306
  async sampleWindowsTree(at) {
8201
8307
  let list;
@@ -8251,7 +8357,7 @@ var RunSampler = class {
8251
8357
  let bytes = 0;
8252
8358
  for (const dir of this.outputDirs) bytes += await dirBytes(dir, budget);
8253
8359
  for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs >= this.startedAt - 1e3)) {
8254
- bytes += await dirBytes(path23.join(this.tmpDir, name), budget);
8360
+ bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
8255
8361
  }
8256
8362
  if (budget.files <= 0) this.peakInUseIsLowerBound = true;
8257
8363
  this.peakInUse = Math.max(this.peakInUse ?? 0, bytes);
@@ -8268,7 +8374,7 @@ var RunSampler = class {
8268
8374
  for (const name of names) {
8269
8375
  if (!PLAYWRIGHT_TEMP.test(name)) continue;
8270
8376
  try {
8271
- if (keep((await fs21.promises.stat(path23.join(this.tmpDir, name))).mtimeMs)) out.push(name);
8377
+ if (keep((await fs21.promises.stat(path21.join(this.tmpDir, name))).mtimeMs)) out.push(name);
8272
8378
  } catch {
8273
8379
  }
8274
8380
  }
@@ -8279,7 +8385,7 @@ var RunSampler = class {
8279
8385
  const budget = { files: WALK_FILE_CAP };
8280
8386
  let bytes = 0;
8281
8387
  for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs < this.startedAt - 1e3)) {
8282
- bytes += await dirBytes(path23.join(this.tmpDir, name), budget);
8388
+ bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
8283
8389
  }
8284
8390
  return bytes;
8285
8391
  } catch {
@@ -8672,7 +8778,7 @@ function resourceTimeline(samples, censuses) {
8672
8778
  let startedAt = hasSamples ? samples.startedAt : Infinity;
8673
8779
  for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
8674
8780
  const shift = hasSamples ? samples.startedAt - startedAt : 0;
8675
- const relative15 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
8781
+ const relative11 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
8676
8782
  const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
8677
8783
  const perWorker = Math.max(
8678
8784
  60,
@@ -8680,17 +8786,17 @@ function resourceTimeline(samples, censuses) {
8680
8786
  );
8681
8787
  return {
8682
8788
  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") : [],
8789
+ cpuPct: hasSamples ? downsamplePoints(relative11(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
8790
+ memoryBytes: hasSamples ? downsamplePoints(relative11(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
8685
8791
  pages: workers.map(({ worker, events }) => {
8686
- const points = relative15(events, -startedAt);
8792
+ const points = relative11(events, -startedAt);
8687
8793
  const last = points.pop();
8688
8794
  return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
8689
8795
  }),
8690
8796
  workers: trees.map((tree) => ({
8691
8797
  worker: tree.worker,
8692
- cpuCores: downsamplePoints(relative15(tree.cpuCores, shift), perWorker, "mean"),
8693
- memoryBytes: downsamplePoints(relative15(tree.memoryBytes, shift), perWorker, "max")
8798
+ cpuCores: downsamplePoints(relative11(tree.cpuCores, shift), perWorker, "mean"),
8799
+ memoryBytes: downsamplePoints(relative11(tree.memoryBytes, shift), perWorker, "max")
8694
8800
  }))
8695
8801
  };
8696
8802
  }
@@ -9252,7 +9358,7 @@ function browserFunctionsInWorker() {
9252
9358
  function browserFunctionCoverage(files) {
9253
9359
  return sanitizeCoverage({
9254
9360
  v: 1,
9255
- files: [...files].map(([path34, functions]) => ({ path: path34, runtime: "browser", functions }))
9361
+ files: [...files].map(([path32, functions]) => ({ path: path32, runtime: "browser", functions }))
9256
9362
  });
9257
9363
  }
9258
9364
  function newKnownFunctions(known, sent) {
@@ -9265,19 +9371,19 @@ function newKnownFunctions(known, sent) {
9265
9371
  budget = MAX_COVERAGE_RANGES;
9266
9372
  };
9267
9373
  for (const [raw, ranges] of known) {
9268
- const path34 = normalizeCoveragePath(raw);
9269
- if (path34 === null) continue;
9270
- let named = sent.get(path34);
9374
+ const path32 = normalizeCoveragePath(raw);
9375
+ if (path32 === null) continue;
9376
+ let named = sent.get(path32);
9271
9377
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
9272
9378
  if (fresh.length === 0) continue;
9273
- if (!named) sent.set(path34, named = /* @__PURE__ */ new Set());
9379
+ if (!named) sent.set(path32, named = /* @__PURE__ */ new Set());
9274
9380
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
9275
9381
  while (fresh.length > 0) {
9276
9382
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
9277
9383
  const part = fresh.slice(0, budget);
9278
9384
  fresh = fresh.slice(part.length);
9279
9385
  budget -= part.length;
9280
- files.push({ path: path34, allFunctions: part });
9386
+ files.push({ path: path32, allFunctions: part });
9281
9387
  }
9282
9388
  }
9283
9389
  close();
@@ -9288,9 +9394,9 @@ function serverCoverageInWorker() {
9288
9394
  }
9289
9395
 
9290
9396
  // src/internal/capture/code-reach.ts
9291
- var crypto4 = __toESM(require("crypto"));
9397
+ var crypto5 = __toESM(require("crypto"));
9292
9398
  var fs22 = __toESM(require("fs"));
9293
- var path24 = __toESM(require("path"));
9399
+ var path22 = __toESM(require("path"));
9294
9400
  var import_node_child_process3 = require("child_process");
9295
9401
 
9296
9402
  // ../core/src/source-map.ts
@@ -9434,14 +9540,6 @@ function decodeSourceMap(raw) {
9434
9540
  return originalAt(line, column)?.source ?? null;
9435
9541
  },
9436
9542
  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
9543
  firstOriginalLineBetween(source, start, end) {
9446
9544
  let first = null;
9447
9545
  eachSegmentBetween(segments, start, end, (i) => {
@@ -9643,12 +9741,12 @@ function repositoryRoot(from) {
9643
9741
  }).trim();
9644
9742
  } catch {
9645
9743
  }
9646
- cachedRepoRoot = { from, root: path24.resolve(root) };
9744
+ cachedRepoRoot = { from, root: path22.resolve(root) };
9647
9745
  return cachedRepoRoot.root;
9648
9746
  }
9649
9747
  function codeReachRoots(configDir, configured) {
9650
9748
  const repoRoot = repositoryRoot(configDir);
9651
- const resolve9 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path24.resolve(configDir, r));
9749
+ const resolve9 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path22.resolve(configDir, r));
9652
9750
  const global = Array.isArray(configured) ? resolve9(configured) : resolve9(configured?.["*"]);
9653
9751
  const byOrigin = /* @__PURE__ */ new Map();
9654
9752
  if (configured && !Array.isArray(configured)) {
@@ -9685,10 +9783,10 @@ function isFile(file) {
9685
9783
  function resolveSourcePath(source, roots) {
9686
9784
  const normalized = normalizeSourcePath(source);
9687
9785
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
9688
- if (path24.isAbsolute(normalized) && isFile(normalized)) return path24.resolve(normalized);
9689
- const relative15 = normalized.replace(/^\/+/, "");
9786
+ if (path22.isAbsolute(normalized) && isFile(normalized)) return path22.resolve(normalized);
9787
+ const relative11 = normalized.replace(/^\/+/, "");
9690
9788
  for (const root of roots) {
9691
- const candidate = path24.resolve(root, relative15);
9789
+ const candidate = path22.resolve(root, relative11);
9692
9790
  if (isFile(candidate)) return candidate;
9693
9791
  }
9694
9792
  return null;
@@ -9720,10 +9818,10 @@ function fileForUrl(url, roots, bases) {
9720
9818
  return file;
9721
9819
  }
9722
9820
  function relativeToRepo(file, repoRoot) {
9723
- return path24.relative(repoRoot, file).split(path24.sep).join("/");
9821
+ return path22.relative(repoRoot, file).split(path22.sep).join("/");
9724
9822
  }
9725
9823
  function scriptVersion(entry) {
9726
- return entry.hash ?? crypto4.createHash("sha1").update(entry.source ?? "").digest("hex");
9824
+ return entry.hash ?? crypto5.createHash("sha1").update(entry.source ?? "").digest("hex");
9727
9825
  }
9728
9826
  async function loadMap(entry, fetchMap) {
9729
9827
  const ref = entry.source ? sourceMappingUrl(entry.source) : null;
@@ -9777,10 +9875,10 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
9777
9875
  const files = /* @__PURE__ */ new Map();
9778
9876
  const known = /* @__PURE__ */ new Map();
9779
9877
  const addTo = (into, file, functions) => {
9780
- const path34 = relativeToRepo(file, roots.repoRoot);
9781
- const listed = into.get(path34);
9878
+ const path32 = relativeToRepo(file, roots.repoRoot);
9879
+ const listed = into.get(path32);
9782
9880
  if (listed) listed.push(...functions);
9783
- else into.set(path34, [...functions]);
9881
+ else into.set(path32, [...functions]);
9784
9882
  };
9785
9883
  const add = (file, functions) => addTo(files, file, functions);
9786
9884
  const bases = viteBases(entries);
@@ -10044,11 +10142,11 @@ async function stopCodeReach(page) {
10044
10142
 
10045
10143
  // src/internal/collect/blob-ids.ts
10046
10144
  var import_node_child_process4 = require("child_process");
10047
- var crypto5 = __toESM(require("crypto"));
10145
+ var crypto6 = __toESM(require("crypto"));
10048
10146
  var fs23 = __toESM(require("fs"));
10049
- var path25 = __toESM(require("path"));
10147
+ var path23 = __toESM(require("path"));
10050
10148
  function gitBlobId(content) {
10051
- return crypto5.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
10149
+ return crypto6.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
10052
10150
  }
10053
10151
  function namesLines(file) {
10054
10152
  return Boolean(file.functions?.length || file.lines?.length || file.executable?.length || file.allFunctions?.length);
@@ -10113,14 +10211,14 @@ var BlobIds = class {
10113
10211
  async read(relativePaths) {
10114
10212
  let root;
10115
10213
  try {
10116
- root = path25.resolve(this.repoRoot());
10214
+ root = path23.resolve(this.repoRoot());
10117
10215
  } catch {
10118
10216
  return relativePaths.map(() => null);
10119
10217
  }
10120
10218
  const files = await Promise.all(
10121
10219
  relativePaths.map(async (relativePath2) => {
10122
- const file = path25.resolve(root, relativePath2);
10123
- if (!file.startsWith(root + path25.sep)) return null;
10220
+ const file = path23.resolve(root, relativePath2);
10221
+ if (!file.startsWith(root + path23.sep)) return null;
10124
10222
  const stat = await fs23.promises.stat(file).catch(() => null);
10125
10223
  return stat?.isFile() ? file : null;
10126
10224
  })
@@ -10129,7 +10227,7 @@ var BlobIds = class {
10129
10227
  const fromGit = /* @__PURE__ */ new Map();
10130
10228
  const ids = forGit.length > 0 ? await this.hashObjects(
10131
10229
  root,
10132
- forGit.map((file) => path25.relative(root, file))
10230
+ forGit.map((file) => path23.relative(root, file))
10133
10231
  ) : null;
10134
10232
  if (ids) forGit.forEach((file, i) => fromGit.set(file, ids[i]));
10135
10233
  return Promise.all(
@@ -10425,11 +10523,11 @@ var CoverageCollection = class {
10425
10523
  };
10426
10524
 
10427
10525
  // src/public/reporter.ts
10428
- function testLocation(test) {
10429
- return `${testFile(test)}:${test.location.line}:${test.location.column}`;
10526
+ function testLocation(test, base) {
10527
+ return `${testFile(test, base)}:${test.location.line}:${test.location.column}`;
10430
10528
  }
10431
- function testFile(test) {
10432
- return path26.relative(process.cwd(), test.location.file).split(path26.sep).join("/");
10529
+ function testFile(test, base) {
10530
+ return relativeTestPath(test.location.file, base);
10433
10531
  }
10434
10532
  var PiwiDashboardReporter = class _PiwiDashboardReporter {
10435
10533
  constructor(rawOptions = {}) {
@@ -10449,6 +10547,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10449
10547
  this.reportedTestIds = /* @__PURE__ */ new Set();
10450
10548
  /** Per-test attempt history, keyed by `test.id`; snapshotted onto every attempt's payload. */
10451
10549
  this.attemptsByTest = /* @__PURE__ */ new Map();
10550
+ /** The folder test files are named relative to: the Playwright config's, set in `onBegin`. */
10551
+ this.testPathBase = process.cwd();
10452
10552
  /** Derived in `onBegin`, once the shard is known. */
10453
10553
  this.instanceId = "";
10454
10554
  this.shardInfo = null;
@@ -10492,7 +10592,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10492
10592
  this.httpClient = new HttpClient(this.options.serverUrl ?? "http://localhost:3000", logger);
10493
10593
  this.fileHandler = new FileHandler(logger);
10494
10594
  this.uploader = new Uploader(this.httpClient, this.fileHandler, logger);
10495
- this.recovery = new CrashRecovery(this.options.projectName, logger);
10595
+ this.recovery = new CrashRecovery(this.httpClient.baseUrl, this.options.projectName, logger);
10496
10596
  this.metadataCollector = new MetadataCollector(logger);
10497
10597
  this.failureLinks = new FailureLinks(this.httpClient.baseUrl, logger);
10498
10598
  const streamBuffer = new StreamBuffer(this.options.projectName);
@@ -10523,6 +10623,15 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10523
10623
  static {
10524
10624
  this.createGlobalSetup = createGlobalSetup;
10525
10625
  }
10626
+ /**
10627
+ * Playwright reporter hook: whether this reporter writes the run's progress to
10628
+ * the terminal. It does not (it prints only its own `[Piwi Dashboard]` lines),
10629
+ * so Playwright keeps its own terminal reporter when Piwi is the only one
10630
+ * configured.
10631
+ */
10632
+ printsToStdio() {
10633
+ return false;
10634
+ }
10526
10635
  /** Playwright reporter hook: called once at the start of the test run */
10527
10636
  onBegin(config, suite) {
10528
10637
  if (this.listMode) {
@@ -10537,10 +10646,11 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10537
10646
  this.logger.info(`Using the desktop app running at ${this.options.serverUrl} (no serverUrl configured).`);
10538
10647
  }
10539
10648
  this.startTime = (/* @__PURE__ */ new Date()).toISOString();
10649
+ this.testPathBase = configDirFromConfig(config);
10540
10650
  this.playwrightVersion = config.version;
10541
10651
  this.maxFailures = config.maxFailures ?? 0;
10542
10652
  if (this.options.captureResources !== false) {
10543
- this.resourcesFile = path26.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
10653
+ this.resourcesFile = path24.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
10544
10654
  process.env[PIWI_RESOURCES_RESULTS_ENV] = this.resourcesFile;
10545
10655
  try {
10546
10656
  this.sampler = new RunSampler({ outputDirs: (config.projects ?? []).map((project) => project.outputDir) });
@@ -10642,7 +10752,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10642
10752
  const beginEvent = {
10643
10753
  type: "begin",
10644
10754
  title: test.title,
10645
- location: testLocation(test),
10755
+ location: testLocation(test, this.testPathBase),
10646
10756
  workerIndex: workerIndexOf(result),
10647
10757
  shardIndex: this.shardInfo?.current ?? null,
10648
10758
  browser: this.metadataCollector.getBrowserConfig(test) || void 0,
@@ -10738,8 +10848,9 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10738
10848
  this.attemptsByTest.set(test.id, attempts);
10739
10849
  const testCase = {
10740
10850
  type: "complete",
10851
+ testId: test.id,
10741
10852
  title: test.title,
10742
- location: testLocation(test),
10853
+ location: testLocation(test, this.testPathBase),
10743
10854
  status,
10744
10855
  duration: result.duration,
10745
10856
  // Effective per-test timeout (reflects project config + describe-level
@@ -10747,7 +10858,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10747
10858
  timeout: test.timeout ?? null,
10748
10859
  // A `test.fail()` test that passed is now `failed` with no recorded error;
10749
10860
  // Playwright reports the same line, so synthesize it.
10750
- error: expectedFailureError(result.status, annotations) ?? buildErrorText(result),
10861
+ error: expectedFailureError(result.status, annotations) ?? buildErrorText(result, this.testPathBase),
10751
10862
  retries: result.retry,
10752
10863
  attempts: attempts.map((a) => ({ ...a })),
10753
10864
  workerIndex: workerIndexOf(result),
@@ -10831,7 +10942,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10831
10942
  if (isFailure && result.retry >= (test.retries ?? 0)) {
10832
10943
  this.failureLinks.add({
10833
10944
  title: test.title,
10834
- file: testFile(test),
10945
+ file: testFile(test, this.testPathBase),
10835
10946
  retry: result.retry,
10836
10947
  browser: typeof testCase.browser?.projectName === "string" ? testCase.browser.projectName : null,
10837
10948
  headline: failureHeadline(testCase.error, testCase.performanceMetrics?.steps)
@@ -10881,7 +10992,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10881
10992
  return;
10882
10993
  }
10883
10994
  if (!result.steps?.length) return;
10884
- const tally = tallyLifecycleSteps(testFile(test), result.steps);
10995
+ const tally = tallyLifecycleSteps(testFile(test, this.testPathBase), result.steps);
10885
10996
  if (tally.opened.length > 0 || tally.closed.context > 0 || tally.closed.browser > 0)
10886
10997
  this.lifecycleTallies.push(tally);
10887
10998
  }
@@ -10944,8 +11055,9 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10944
11055
  const locks = collectTestLocks(test);
10945
11056
  const testCase = {
10946
11057
  type: "complete",
11058
+ testId: test.id,
10947
11059
  title: test.title,
10948
- location: testLocation(test),
11060
+ location: testLocation(test, this.testPathBase),
10949
11061
  status: "didnotrun",
10950
11062
  duration: 0,
10951
11063
  timeout: test.timeout ?? null,
@@ -11022,6 +11134,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11022
11134
  const branch = resolveScmBranch(process.env) ?? null;
11023
11135
  await this.manifests.upload(this.httpClient, submitted.auth, this.options.projectName, branch);
11024
11136
  }
11137
+ } catch (error) {
11138
+ this.logger.error(`Could not report the run to the dashboard: ${errorMessage(error)}`);
11025
11139
  } finally {
11026
11140
  this.coverage?.close();
11027
11141
  this.fileHandler.cleanupBodyAttachments();
@@ -11103,7 +11217,7 @@ async function resolveSelection(options = {}) {
11103
11217
  }
11104
11218
 
11105
11219
  // src/internal/capture/capture-fixtures.ts
11106
- var path30 = __toESM(require("path"));
11220
+ var path28 = __toESM(require("path"));
11107
11221
  var import_node_zlib2 = require("zlib");
11108
11222
 
11109
11223
  // ../picker-dom/src/probe.ts
@@ -11534,26 +11648,26 @@ function installPickerOverlay(arg) {
11534
11648
  }
11535
11649
  };
11536
11650
  mount(highlight, label, banner);
11537
- const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
11651
+ const quoteJs = (s) => `'${JSON.stringify(s).slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
11538
11652
  const describe = (el) => {
11539
11653
  const tag = (el.tagName || "?").toLowerCase();
11540
11654
  const testId = el.getAttribute && el.getAttribute(testIdAttr);
11541
- if (testId) return `getByTestId('${escJs(testId)}')`;
11655
+ if (testId) return `getByTestId(${quoteJs(testId)})`;
11542
11656
  if (el.labels && el.labels.length > 0) {
11543
11657
  const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
11544
- if (labelText) return `getByLabel('${escJs(labelText)}')`;
11658
+ if (labelText) return `getByLabel(${quoteJs(labelText)})`;
11545
11659
  }
11546
11660
  const ariaLabel = el.getAttribute && el.getAttribute("aria-label");
11547
- if (ariaLabel) return `getByLabel('${escJs(ariaLabel)}')`;
11661
+ if (ariaLabel) return `getByLabel(${quoteJs(ariaLabel)})`;
11548
11662
  const placeholder = el.getAttribute && el.getAttribute("placeholder");
11549
- if (placeholder) return `getByPlaceholder('${escJs(placeholder)}')`;
11663
+ if (placeholder) return `getByPlaceholder(${quoteJs(placeholder)})`;
11550
11664
  const alt = el.getAttribute && el.getAttribute("alt");
11551
- if (alt) return `getByAltText('${escJs(alt)}')`;
11665
+ if (alt) return `getByAltText(${quoteJs(alt)})`;
11552
11666
  const titleAttr = el.getAttribute && el.getAttribute("title");
11553
- if (titleAttr) return `getByTitle('${escJs(titleAttr)}')`;
11554
- if (el.id) return `locator('#${escJs(el.id)}')`;
11667
+ if (titleAttr) return `getByTitle(${quoteJs(titleAttr)})`;
11668
+ if (el.id) return `locator(${quoteJs(`#${el.id}`)})`;
11555
11669
  const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
11556
- return cls ? `locator('.${escJs(cls)}')` : tag;
11670
+ return cls ? `locator(${quoteJs(`.${cls}`)})` : tag;
11557
11671
  };
11558
11672
  const buildChain = (raw) => {
11559
11673
  const chain2 = [];
@@ -12339,7 +12453,6 @@ function headingLevel(attrs, role) {
12339
12453
  return null;
12340
12454
  }
12341
12455
  var attr = (a, key) => a.attributes[key] || null;
12342
- var esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
12343
12456
  var escCssAttrValue = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
12344
12457
  var isCssSafeId = (id) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(id);
12345
12458
  function classifyCssStability(className) {
@@ -12394,7 +12507,7 @@ function generateAlternatives(attrs, options = {}) {
12394
12507
  const testId = attr(attrs, testIdAttribute);
12395
12508
  if (testId && isUnique(counts?.testId)) {
12396
12509
  add({
12397
- locator: `getByTestId('${esc(testId)}')`,
12510
+ locator: `getByTestId(${quoteJsString(testId)})`,
12398
12511
  method: "getByTestId",
12399
12512
  args: { testId },
12400
12513
  score: 100
@@ -12405,7 +12518,7 @@ function generateAlternatives(attrs, options = {}) {
12405
12518
  const withLevel = (base) => level != null ? { ...base, level } : base;
12406
12519
  if (role && accessibleName) {
12407
12520
  add({
12408
- locator: `getByRole('${role}', { name: '${esc(accessibleName)}'${levelPart} })`,
12521
+ locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(accessibleName)}${levelPart} })`,
12409
12522
  method: "getByRole",
12410
12523
  args: withLevel({ role, name: accessibleName }),
12411
12524
  score: 90 - ambiguityPenalty(counts?.roleName)
@@ -12414,7 +12527,7 @@ function generateAlternatives(attrs, options = {}) {
12414
12527
  const ariaLabel = attr(attrs, "aria-label");
12415
12528
  if (role && ariaLabel && ariaLabel !== accessibleName) {
12416
12529
  add({
12417
- locator: `getByRole('${role}', { name: '${esc(ariaLabel)}'${levelPart} })`,
12530
+ locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(ariaLabel)}${levelPart} })`,
12418
12531
  method: "getByRole",
12419
12532
  args: withLevel({ role, name: ariaLabel }),
12420
12533
  score: 85 - ambiguityPenalty(counts?.roleName)
@@ -12425,7 +12538,7 @@ function generateAlternatives(attrs, options = {}) {
12425
12538
  const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
12426
12539
  if (labelBacked) {
12427
12540
  add({
12428
- locator: `getByLabel('${esc(accessibleName)}')`,
12541
+ locator: `getByLabel(${quoteJsString(accessibleName)})`,
12429
12542
  method: "getByLabel",
12430
12543
  args: { label: accessibleName },
12431
12544
  score: 85
@@ -12435,7 +12548,7 @@ function generateAlternatives(attrs, options = {}) {
12435
12548
  const placeholder = attr(attrs, "placeholder");
12436
12549
  if (placeholder) {
12437
12550
  add({
12438
- locator: `getByPlaceholder('${esc(placeholder)}')`,
12551
+ locator: `getByPlaceholder(${quoteJsString(placeholder)})`,
12439
12552
  method: "getByPlaceholder",
12440
12553
  args: { placeholder },
12441
12554
  score: 80 - ambiguityPenalty(counts?.placeholder)
@@ -12443,7 +12556,7 @@ function generateAlternatives(attrs, options = {}) {
12443
12556
  }
12444
12557
  if (text && text.length < 80) {
12445
12558
  add({
12446
- locator: `getByText('${esc(text)}')`,
12559
+ locator: `getByText(${quoteJsString(text)})`,
12447
12560
  method: "getByText",
12448
12561
  args: { text },
12449
12562
  score: 75 - ambiguityPenalty(counts?.text)
@@ -12453,7 +12566,7 @@ function generateAlternatives(attrs, options = {}) {
12453
12566
  if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
12454
12567
  const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
12455
12568
  add({
12456
- locator: `locator('${esc(selector)}')`,
12569
+ locator: `locator(${quoteJsString(selector)})`,
12457
12570
  method: "locator",
12458
12571
  args: { selector },
12459
12572
  score: 65
@@ -12463,7 +12576,7 @@ function generateAlternatives(attrs, options = {}) {
12463
12576
  if (name && selectable("name") && isUnique(counts?.name)) {
12464
12577
  const selector = `[name="${escCssAttrValue(name)}"]`;
12465
12578
  add({
12466
- locator: `locator('${esc(selector)}')`,
12579
+ locator: `locator(${quoteJsString(selector)})`,
12467
12580
  method: "locator",
12468
12581
  args: { selector },
12469
12582
  score: 60
@@ -12472,7 +12585,7 @@ function generateAlternatives(attrs, options = {}) {
12472
12585
  const alt = attr(attrs, "alt");
12473
12586
  if (alt) {
12474
12587
  add({
12475
- locator: `getByAltText('${esc(alt)}')`,
12588
+ locator: `getByAltText(${quoteJsString(alt)})`,
12476
12589
  method: "getByAltText",
12477
12590
  args: { text: alt },
12478
12591
  score: 60 - ambiguityPenalty(counts?.alt)
@@ -12481,7 +12594,7 @@ function generateAlternatives(attrs, options = {}) {
12481
12594
  const title = attr(attrs, "title");
12482
12595
  if (title) {
12483
12596
  add({
12484
- locator: `getByTitle('${esc(title)}')`,
12597
+ locator: `getByTitle(${quoteJsString(title)})`,
12485
12598
  method: "getByTitle",
12486
12599
  args: { title },
12487
12600
  score: 50 - ambiguityPenalty(counts?.title)
@@ -12499,7 +12612,7 @@ function generateAlternatives(attrs, options = {}) {
12499
12612
  if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
12500
12613
  testIdAnchorDone = true;
12501
12614
  add({
12502
- locator: `getByTestId('${esc(anc.testId)}').${leaf.expr}`,
12615
+ locator: `getByTestId(${quoteJsString(anc.testId)}).${leaf.expr}`,
12503
12616
  method: leaf.method,
12504
12617
  args: { ...leaf.args, anchorTestId: anc.testId },
12505
12618
  score: scores.testId
@@ -12509,7 +12622,7 @@ function generateAlternatives(attrs, options = {}) {
12509
12622
  idAnchorDone = true;
12510
12623
  const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
12511
12624
  add({
12512
- locator: `locator('${esc(anchorSelector)}').${leaf.expr}`,
12625
+ locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
12513
12626
  method: leaf.method,
12514
12627
  args: { ...leaf.args, anchorSelector },
12515
12628
  score: scores.id
@@ -12520,7 +12633,7 @@ function generateAlternatives(attrs, options = {}) {
12520
12633
  const { name: name2, value } = anc.dataAttr;
12521
12634
  const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
12522
12635
  add({
12523
- locator: `locator('${esc(anchorSelector)}').${leaf.expr}`,
12636
+ locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
12524
12637
  method: leaf.method,
12525
12638
  args: { ...leaf.args, anchorSelector },
12526
12639
  score: TEST_DATA_ATTRS.has(name2) ? scores.testData : scores.data
@@ -12530,7 +12643,7 @@ function generateAlternatives(attrs, options = {}) {
12530
12643
  if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
12531
12644
  roleAnchorDone = true;
12532
12645
  add({
12533
- locator: `getByRole('${esc(ancestorRole)}').${leaf.expr}`,
12646
+ locator: `getByRole(${quoteJsString(ancestorRole)}).${leaf.expr}`,
12534
12647
  method: leaf.method,
12535
12648
  args: { ...leaf.args, anchorRole: ancestorRole },
12536
12649
  score: scores.role
@@ -12539,7 +12652,7 @@ function generateAlternatives(attrs, options = {}) {
12539
12652
  if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
12540
12653
  filterAnchorDone = true;
12541
12654
  add({
12542
- locator: `getByRole('${esc(ancestorRole)}').filter({ hasText: '${esc(anc.filterText)}' }).${leaf.expr}`,
12655
+ locator: `getByRole(${quoteJsString(ancestorRole)}).filter({ hasText: ${quoteJsString(anc.filterText)} }).${leaf.expr}`,
12543
12656
  method: leaf.method,
12544
12657
  args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
12545
12658
  score: scores.filter
@@ -12548,7 +12661,7 @@ function generateAlternatives(attrs, options = {}) {
12548
12661
  }
12549
12662
  };
12550
12663
  if (role && !hasOwnTestId) {
12551
- const rolePart = level != null ? `'${role}', { level: ${level} }` : `'${role}'`;
12664
+ const rolePart = level != null ? `${quoteJsString(role)}, { level: ${level} }` : quoteJsString(role);
12552
12665
  const leafArgs = withLevel({ role });
12553
12666
  addAnchoredChains(
12554
12667
  { expr: `getByRole(${rolePart})`, method: "getByRole", args: leafArgs, role },
@@ -12566,7 +12679,7 @@ function generateAlternatives(attrs, options = {}) {
12566
12679
  }
12567
12680
  } else if (!role && text && text.length < 80 && !hasOwnTestId) {
12568
12681
  addAnchoredChains(
12569
- { expr: `getByText('${esc(text)}')`, method: "getByText", args: { text }, role: null },
12682
+ { expr: `getByText(${quoteJsString(text)})`, method: "getByText", args: { text }, role: null },
12570
12683
  (anc) => anc.scopedTextCount,
12571
12684
  { testId: 68, testData: 66, id: 60, data: 56, role: 51, filter: 49 }
12572
12685
  );
@@ -12577,7 +12690,7 @@ function generateAlternatives(attrs, options = {}) {
12577
12690
  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
12691
  for (const { cls, score } of classes) {
12579
12692
  add({
12580
- locator: `locator('.${esc(cls)}')`,
12693
+ locator: `locator(${quoteJsString(`.${cls}`)})`,
12581
12694
  method: "locator",
12582
12695
  args: { selector: `.${cls}` },
12583
12696
  score
@@ -12605,26 +12718,25 @@ function approximateAccessibleName(attrs) {
12605
12718
  }
12606
12719
 
12607
12720
  // ../picker-dom/src/anchor-alternatives.ts
12608
- var escStr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
12609
12721
  var cssIdSelector = (id) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id) ? `#${id}` : `[id="${id.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"]`;
12610
12722
  var ANCHOR_KIND_SCORES = { testid: 80, id: 74, labeledRole: 68, role: 62 };
12611
12723
  function anchorSegment(a) {
12612
12724
  if (a.testId && a.testIdCount === 1) {
12613
- return { code: `getByTestId('${escStr(a.testId)}')`, kind: "testid", flatArgs: { anchorTestId: a.testId } };
12725
+ return { code: `getByTestId(${quoteJsString(a.testId)})`, kind: "testid", flatArgs: { anchorTestId: a.testId } };
12614
12726
  }
12615
12727
  if (a.id && !isAutoGenerated(a.id) && a.idCount === 1) {
12616
12728
  const anchorSelector = cssIdSelector(a.id);
12617
- return { code: `locator('${escStr(anchorSelector)}')`, kind: "id", flatArgs: { anchorSelector } };
12729
+ return { code: `locator(${quoteJsString(anchorSelector)})`, kind: "id", flatArgs: { anchorSelector } };
12618
12730
  }
12619
12731
  if (a.role && a.ariaLabel && a.labeledRoleCount === 1) {
12620
12732
  return {
12621
- code: `getByRole('${escStr(a.role)}', { name: '${escStr(a.ariaLabel)}' })`,
12733
+ code: `getByRole(${quoteJsString(a.role)}, { name: ${quoteJsString(a.ariaLabel)} })`,
12622
12734
  kind: "labeledRole",
12623
12735
  flatArgs: { anchorRole: a.role, anchorName: a.ariaLabel }
12624
12736
  };
12625
12737
  }
12626
12738
  if (a.role && a.roleCount === 1) {
12627
- return { code: `getByRole('${escStr(a.role)}')`, kind: "role", flatArgs: { anchorRole: a.role } };
12739
+ return { code: `getByRole(${quoteJsString(a.role)})`, kind: "role", flatArgs: { anchorRole: a.role } };
12628
12740
  }
12629
12741
  return null;
12630
12742
  }
@@ -12632,7 +12744,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
12632
12744
  if (!leaf.role || anchors.length === 0) return [];
12633
12745
  const out = [];
12634
12746
  const seen = /* @__PURE__ */ new Set();
12635
- const rolePart = leaf.level != null ? `'${leaf.role}', { level: ${leaf.level} }` : `'${leaf.role}'`;
12747
+ const rolePart = leaf.level != null ? `${quoteJsString(leaf.role)}, { level: ${leaf.level} }` : quoteJsString(leaf.role);
12636
12748
  const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
12637
12749
  const add = (l) => {
12638
12750
  if (!seen.has(l.locator)) {
@@ -12687,21 +12799,21 @@ var MAX_INDETERMINATE = 20;
12687
12799
  var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
12688
12800
  var LEVEL = `${STEP}(?:/${STEP})*`;
12689
12801
  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);
12802
+ function isElementPath(path32) {
12803
+ return typeof path32 === "string" && path32.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path32);
12692
12804
  }
12693
12805
  var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
12694
12806
  function elementRef(v) {
12695
12807
  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) : "" };
12808
+ const { path: path32, label } = v;
12809
+ if (!isElementPath(path32)) return null;
12810
+ return { path: path32, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
12699
12811
  }
12700
12812
  function textPoint(v) {
12701
12813
  if (!v || typeof v !== "object") return null;
12702
- const { path: path34, offset } = v;
12814
+ const { path: path32, offset } = v;
12703
12815
  const at = offsetOf(offset);
12704
- return isElementPath(path34) && at !== null ? { path: path34, offset: at } : null;
12816
+ return isElementPath(path32) && at !== null ? { path: path32, offset: at } : null;
12705
12817
  }
12706
12818
  function sanitizePageInteraction(raw) {
12707
12819
  if (!raw || typeof raw !== "object") return {};
@@ -12732,7 +12844,7 @@ function sanitizePageInteraction(raw) {
12732
12844
  }
12733
12845
 
12734
12846
  // src/internal/capture/locator-healing.ts
12735
- var path27 = __toESM(require("path"));
12847
+ var path25 = __toESM(require("path"));
12736
12848
 
12737
12849
  // ../core/src/locator-fingerprint.ts
12738
12850
  var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
@@ -12758,7 +12870,6 @@ var TEXT_CONTENT_ROLES = /* @__PURE__ */ new Set([
12758
12870
  "switch"
12759
12871
  ]);
12760
12872
  var FORM_FIELD_ROLES = /* @__PURE__ */ new Set(["textbox", "combobox", "searchbox", "spinbutton", "slider"]);
12761
- var escapeQuote = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
12762
12873
  function parseAriaCandidates(ariaSnapshot) {
12763
12874
  if (!ariaSnapshot) return [];
12764
12875
  const out = [];
@@ -12786,6 +12897,23 @@ function textSimilarity(a, b) {
12786
12897
  for (const t of sa) if (sb.has(t)) common++;
12787
12898
  return 2 * common / (sa.size + sb.size);
12788
12899
  }
12900
+ var FUNCTION_WORDS = new Set(
12901
+ "the and for with from your you our this that les des une pour avec par sur aux dans vos votre nos notre".split(" ")
12902
+ );
12903
+ function significantWords(text) {
12904
+ const all = (text ?? "").replace(new RegExp("(\\p{Ll})(\\p{Lu})", "gu"), "$1 $2").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
12905
+ const meaningful = all.filter((w) => w.length >= 3 && !FUNCTION_WORDS.has(w));
12906
+ return new Set(meaningful.length > 0 ? meaningful : all);
12907
+ }
12908
+ function significantWordSimilarity(a, b) {
12909
+ const sa = significantWords(a);
12910
+ const sb = significantWords(b);
12911
+ if (sa.size === 0 && sb.size === 0) return 1;
12912
+ if (sa.size === 0 || sb.size === 0) return 0;
12913
+ let common = 0;
12914
+ for (const w of sa) if (sb.has(w)) common++;
12915
+ return 2 * common / (sa.size + sb.size);
12916
+ }
12789
12917
  function fingerprintPresent(fp, candidates) {
12790
12918
  if (!fp.name) return false;
12791
12919
  return candidates.some(
@@ -12807,7 +12935,7 @@ function matchRenamedElement(fp, candidates) {
12807
12935
  let best = null;
12808
12936
  let bestScore = -1;
12809
12937
  for (const c of pool) {
12810
- const s = textSimilarity(c.name, fp.name);
12938
+ const s = significantWordSimilarity(c.name, fp.name);
12811
12939
  if (s > bestScore) {
12812
12940
  bestScore = s;
12813
12941
  best = c;
@@ -12827,12 +12955,12 @@ function freshLocatorsFromCandidate(c) {
12827
12955
  if (!name) return out;
12828
12956
  out.push(
12829
12957
  c.level != null ? {
12830
- locator: `getByRole('${escapeQuote(c.role)}', { name: '${escapeQuote(name)}', level: ${c.level} })`,
12958
+ locator: `getByRole(${quoteJsString(c.role)}, { name: ${quoteJsString(name)}, level: ${c.level} })`,
12831
12959
  method: "getByRole",
12832
12960
  args: { role: c.role, name, level: c.level },
12833
12961
  score: ELEMENT_MATCH_SCORES.role
12834
12962
  } : {
12835
- locator: `getByRole('${escapeQuote(c.role)}', { name: '${escapeQuote(name)}' })`,
12963
+ locator: `getByRole(${quoteJsString(c.role)}, { name: ${quoteJsString(name)} })`,
12836
12964
  method: "getByRole",
12837
12965
  args: { role: c.role, name },
12838
12966
  score: ELEMENT_MATCH_SCORES.role
@@ -12840,14 +12968,14 @@ function freshLocatorsFromCandidate(c) {
12840
12968
  );
12841
12969
  if (TEXT_CONTENT_ROLES.has(c.role)) {
12842
12970
  out.push({
12843
- locator: `getByText('${escapeQuote(name)}')`,
12971
+ locator: `getByText(${quoteJsString(name)})`,
12844
12972
  method: "getByText",
12845
12973
  args: { text: name },
12846
12974
  score: ELEMENT_MATCH_SCORES.text
12847
12975
  });
12848
12976
  } else if (FORM_FIELD_ROLES.has(c.role)) {
12849
12977
  out.push({
12850
- locator: `getByLabel('${escapeQuote(name)}')`,
12978
+ locator: `getByLabel(${quoteJsString(name)})`,
12851
12979
  method: "getByLabel",
12852
12980
  args: { label: name },
12853
12981
  score: ELEMENT_MATCH_SCORES.label
@@ -12992,7 +13120,6 @@ var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
12992
13120
  "getByTitle",
12993
13121
  "getByAltText"
12994
13122
  ]);
12995
- var escAttr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
12996
13123
  var SUGG_TEXT_ROLES = /* @__PURE__ */ new Set([
12997
13124
  "button",
12998
13125
  "link",
@@ -13025,9 +13152,9 @@ function failedNameAndRole(failed) {
13025
13152
  function renderFailing(failed) {
13026
13153
  const { role, name } = failedNameAndRole(failed);
13027
13154
  if (failed.method === "getByRole") {
13028
- return name ? `getByRole('${escAttr(role ?? "")}', { name: '${escAttr(name)}' })` : `getByRole('${escAttr(role ?? "")}')`;
13155
+ return name ? `getByRole(${quoteJsString(role ?? "")}, { name: ${quoteJsString(name)} })` : `getByRole(${quoteJsString(role ?? "")})`;
13029
13156
  }
13030
- return `${failed.method}('${escAttr(name ?? "")}')`;
13157
+ return `${failed.method}(${quoteJsString(name ?? "")})`;
13031
13158
  }
13032
13159
  function freshSuggestions(candidate, failedMethod) {
13033
13160
  const out = [];
@@ -13037,9 +13164,9 @@ function freshSuggestions(candidate, failedMethod) {
13037
13164
  if (!out.includes(s)) out.push(s);
13038
13165
  };
13039
13166
  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)}')`;
13167
+ const roleLoc = `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(name)}${levelPart} })`;
13168
+ const textLoc = `getByText(${quoteJsString(name)})`;
13169
+ const labelLoc = `getByLabel(${quoteJsString(name)})`;
13043
13170
  if (failedMethod === "getByText" && SUGG_TEXT_ROLES.has(role)) push(textLoc);
13044
13171
  if (failedMethod === "getByLabel" && SUGG_FIELD_ROLES.has(role)) push(labelLoc);
13045
13172
  push(roleLoc);
@@ -13102,10 +13229,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
13102
13229
  }
13103
13230
  let rel = file;
13104
13231
  try {
13105
- rel = path27.relative(process.cwd(), file);
13232
+ rel = path25.relative(process.cwd(), file);
13106
13233
  } catch {
13107
13234
  }
13108
- rel = rel.split(path27.sep).join("/");
13235
+ rel = rel.split(path25.sep).join("/");
13109
13236
  if (rel.startsWith("./")) rel = rel.slice(2);
13110
13237
  return `${rel}:${m[3]}:${m[4]}`;
13111
13238
  }
@@ -13335,8 +13462,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
13335
13462
  }
13336
13463
 
13337
13464
  // src/internal/capture/pick-on-failure.ts
13338
- var path28 = __toESM(require("path"));
13339
- var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
13465
+ var path26 = __toESM(require("path"));
13340
13466
  function parseLeafLocatorExpression(rawExpr) {
13341
13467
  const leaf = parseLeafLocatorCall(rawExpr);
13342
13468
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -13345,14 +13471,13 @@ function deriveFailedLocator(testInfo) {
13345
13471
  const info = testInfo;
13346
13472
  const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
13347
13473
  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());
13474
+ const header = readLocatorHeader(stripAnsi(`${err.message ?? ""}
13475
+ ${err.stack ?? ""}`));
13476
+ if (!header) continue;
13477
+ const parsed2 = parseLeafLocatorExpression(header);
13353
13478
  if (!parsed2) continue;
13354
13479
  const loc = err.location;
13355
- const location = loc ? `${path28.relative(process.cwd(), loc.file).split(path28.sep).join("/")}:${loc.line}:${loc.column}` : null;
13480
+ const location = loc ? `${path26.relative(process.cwd(), loc.file).split(path26.sep).join("/")}:${loc.line}:${loc.column}` : null;
13356
13481
  return { method: parsed2.method, args: parsed2.args, location };
13357
13482
  }
13358
13483
  return null;
@@ -13505,7 +13630,7 @@ function applyPickToSnapshots(snapshots, pick) {
13505
13630
  }
13506
13631
 
13507
13632
  // src/internal/capture/pause-at.ts
13508
- var path29 = __toESM(require("path"));
13633
+ var path27 = __toESM(require("path"));
13509
13634
 
13510
13635
  // ../core/src/editor-send.ts
13511
13636
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
@@ -13549,9 +13674,9 @@ var PauseSet = class {
13549
13674
  var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
13550
13675
  function callSiteFile(file, cwd) {
13551
13676
  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, "/");
13677
+ const absolute = path27.isAbsolute(file) || path27.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
13678
+ let rel = absolute ? path27.relative(cwd, file) : path27.normalize(slashed);
13679
+ rel = rel.split(path27.sep).join("/").replace(/\\/g, "/");
13555
13680
  if (rel.startsWith("./")) rel = rel.slice(2);
13556
13681
  return rel;
13557
13682
  }
@@ -14035,7 +14160,7 @@ function configuredCodeReachRoots() {
14035
14160
  }
14036
14161
  } catch {
14037
14162
  }
14038
- return raw.split(path30.delimiter).filter(Boolean);
14163
+ return raw.split(path28.delimiter).filter(Boolean);
14039
14164
  }
14040
14165
  async function stopSinkCodeReach(sink, closing) {
14041
14166
  const stopping = [];
@@ -14051,9 +14176,7 @@ async function stopSinkCodeReach(sink, closing) {
14051
14176
  }
14052
14177
  if (stopping.length > 0) {
14053
14178
  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());
14179
+ const roots = codeReachRoots(configDirFromConfig(info?.config), configuredCodeReachRoots());
14057
14180
  for (const [page, session] of stopping) {
14058
14181
  session.stopping = stopPageCodeReach(sink, page, session, roots);
14059
14182
  underway.push(session.stopping);
@@ -14195,8 +14318,8 @@ function buildPageState(raw, cookies, interaction) {
14195
14318
  length: typeof e.length === "number" ? e.length : 0
14196
14319
  }));
14197
14320
  return {
14198
- url: raw.url,
14199
- hash: raw.hash || null,
14321
+ url: raw.url.split("#")[0],
14322
+ hash: raw.hash ? maskUrlFragment(raw.hash) : null,
14200
14323
  historyState: historyState || null,
14201
14324
  localStorage: capStorage(raw.localStorage),
14202
14325
  sessionStorage: capStorage(raw.sessionStorage),
@@ -15213,6 +15336,7 @@ var piwiFixtures = {
15213
15336
  const plan = loadFlakePlan();
15214
15337
  const role = flakeRoleForTest(plan, {
15215
15338
  file: testInfo.file,
15339
+ base: configDirFromConfig(testInfo.config),
15216
15340
  title: testInfo.title,
15217
15341
  titlePath: testInfo.titlePath,
15218
15342
  project: testInfo.project.name
@@ -15226,7 +15350,8 @@ var piwiFixtures = {
15226
15350
  sink.probeItem = probeItemForTest({
15227
15351
  title: testInfo.title,
15228
15352
  file: testInfo.file,
15229
- titlePath: testInfo.titlePath
15353
+ titlePath: testInfo.titlePath,
15354
+ base: configDirFromConfig(testInfo.config)
15230
15355
  });
15231
15356
  activeResourceLedger()?.testStarted(testInfo);
15232
15357
  currentSink = sink;
@@ -15270,6 +15395,7 @@ async function recordFlakeAttempt(flake, testInfo) {
15270
15395
  flakeResultLine(flake.plan, {
15271
15396
  role: flake.role,
15272
15397
  file: testInfo.file,
15398
+ base: configDirFromConfig(testInfo.config),
15273
15399
  title: testInfo.title,
15274
15400
  project: testInfo.project.name || null,
15275
15401
  browserName: flake.browserName,
@@ -15291,12 +15417,12 @@ function extendPiwiFixtures(test) {
15291
15417
 
15292
15418
  // src/internal/ai/ai-fixtures.ts
15293
15419
  var fs26 = __toESM(require("fs"));
15294
- var path33 = __toESM(require("path"));
15420
+ var path31 = __toESM(require("path"));
15295
15421
  var import_test = require("@playwright/test");
15296
15422
 
15297
15423
  // src/internal/ai/artifact.ts
15298
15424
  var fs25 = __toESM(require("fs"));
15299
- var path31 = __toESM(require("path"));
15425
+ var path29 = __toESM(require("path"));
15300
15426
  var ARTIFACT_VERSION = 1;
15301
15427
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
15302
15428
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -15400,7 +15526,7 @@ function writeEntry(file, entry) {
15400
15526
  const canonical = serializeEntry(entry);
15401
15527
  return withEntryLock(file, () => {
15402
15528
  if (fs25.existsSync(file) && fs25.readFileSync(file, "utf8") === canonical) return { written: false };
15403
- fs25.mkdirSync(path31.dirname(file), { recursive: true });
15529
+ fs25.mkdirSync(path29.dirname(file), { recursive: true });
15404
15530
  const tmp = `${file}.${process.pid}.tmp`;
15405
15531
  fs25.writeFileSync(tmp, canonical);
15406
15532
  fs25.renameSync(tmp, file);
@@ -15409,7 +15535,7 @@ function writeEntry(file, entry) {
15409
15535
  }
15410
15536
  function withEntryLock(file, fn) {
15411
15537
  const lock = `${file}.lock`;
15412
- fs25.mkdirSync(path31.dirname(file), { recursive: true });
15538
+ fs25.mkdirSync(path29.dirname(file), { recursive: true });
15413
15539
  const deadline = Date.now() + 5e3;
15414
15540
  for (; ; ) {
15415
15541
  try {
@@ -15437,28 +15563,28 @@ function withEntryLock(file, fn) {
15437
15563
  }
15438
15564
 
15439
15565
  // src/internal/ai/keys.ts
15440
- var crypto6 = __toESM(require("crypto"));
15441
- var path32 = __toESM(require("path"));
15566
+ var crypto7 = __toESM(require("crypto"));
15567
+ var path30 = __toESM(require("path"));
15442
15568
  var DEFAULT_AI_DIR = "__piwi__";
15443
15569
  function normalizeTemplate(template) {
15444
15570
  return template.trim().replace(/\s+/g, " ").toLowerCase();
15445
15571
  }
15446
15572
  function hashTemplate(normalized, ordinal = 0) {
15447
15573
  const input = ordinal > 0 ? `${normalized}#${ordinal}` : normalized;
15448
- return crypto6.createHash("sha256").update(input).digest("hex").slice(0, 8);
15574
+ return crypto7.createHash("sha256").update(input).digest("hex").slice(0, 8);
15449
15575
  }
15450
15576
  function slug(text, maxLength = 40) {
15451
15577
  const base = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
15452
15578
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
15453
15579
  }
15454
15580
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
15455
- return path32.join(path32.dirname(specFile), dir, path32.basename(specFile));
15581
+ return path30.join(path30.dirname(specFile), dir, path30.basename(specFile));
15456
15582
  }
15457
15583
  function entryPath(params) {
15458
15584
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
15459
15585
  const hash = hashTemplate(basis, params.ordinal ?? 0);
15460
15586
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
15461
- return path32.join(entryDir(params.specFile, params.dir), name);
15587
+ return path30.join(entryDir(params.specFile, params.dir), name);
15462
15588
  }
15463
15589
  function parseLocation2(location) {
15464
15590
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -15865,6 +15991,8 @@ async function groundElement(page, element, params) {
15865
15991
  // src/internal/ai/resolution.ts
15866
15992
  var MAX_SNAPSHOT_CHARS = 24e3;
15867
15993
  var DEFAULT_MAX_STEPS = 20;
15994
+ var MAX_OBSERVED_RESPONSES = 50;
15995
+ var MAX_OBSERVED_URL_CHARS = 4e3;
15868
15996
  var ServerStepResolver = class {
15869
15997
  constructor(serverUrl, apiKey) {
15870
15998
  this.serverUrl = serverUrl;
@@ -16016,14 +16144,15 @@ async function resolveRun(template, ctx) {
16016
16144
  ctx.responseWaitTimeout
16017
16145
  );
16018
16146
  history.push({ action: decision.action, element: decision.element, value: decision.value });
16019
- if (observed.length > 0) {
16147
+ const observedResponses = [...new Set(observed.map((url) => maskValues(url, ctx.params)))].filter((url) => url.length <= MAX_OBSERVED_URL_CHARS).slice(0, MAX_OBSERVED_RESPONSES);
16148
+ if (observedResponses.length > 0) {
16020
16149
  const waitDecision = await ctx.resolver.resolveStep({
16021
16150
  kind: "wait",
16022
16151
  template,
16023
16152
  paramNames,
16024
16153
  ariaSnapshot: "",
16025
16154
  history,
16026
- observedResponses: observed.map((url) => maskValues(url, ctx.params))
16155
+ observedResponses
16027
16156
  });
16028
16157
  if (waitDecision.waitForResponse) step.waitForResponse = waitDecision.waitForResponse;
16029
16158
  }
@@ -16058,7 +16187,7 @@ function testIdentity(testInfo) {
16058
16187
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
16059
16188
  }
16060
16189
  function relativeToCwd(file) {
16061
- return path33.relative(process.cwd(), file).split(path33.sep).join("/");
16190
+ return path31.relative(process.cwd(), file).split(path31.sep).join("/");
16062
16191
  }
16063
16192
  function recordIntents(intents, entry) {
16064
16193
  const add = (locator, kind) => {