@piwitests/reporter 0.58.0 → 0.60.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
@@ -43,9 +43,12 @@ __export(index_exports, {
43
43
  module.exports = __toCommonJS(index_exports);
44
44
 
45
45
  // src/public/reporter.ts
46
- var fs24 = __toESM(require("fs"));
46
+ var fs25 = __toESM(require("fs"));
47
47
  var os9 = __toESM(require("os"));
48
- var path24 = __toESM(require("path"));
48
+ var path25 = __toESM(require("path"));
49
+
50
+ // src/internal/config/env.ts
51
+ var path2 = __toESM(require("path"));
49
52
 
50
53
  // src/internal/config/desktop.ts
51
54
  var fs = __toESM(require("fs"));
@@ -54,6 +57,14 @@ var path = __toESM(require("path"));
54
57
  function defaultDesktopConfigPath() {
55
58
  return path.join(os.homedir(), ".piwi", "desktop.json");
56
59
  }
60
+ function processAlive(pid) {
61
+ try {
62
+ process.kill(pid, 0);
63
+ return true;
64
+ } catch (error) {
65
+ return error.code === "EPERM";
66
+ }
67
+ }
57
68
  function readDesktopConfig(filePath2) {
58
69
  try {
59
70
  const parsed2 = JSON.parse(fs.readFileSync(filePath2, "utf8"));
@@ -61,12 +72,75 @@ function readDesktopConfig(filePath2) {
61
72
  const token = parsed2?.token;
62
73
  if (typeof url !== "string" || !url) return null;
63
74
  if (typeof token !== "string" || !token) return null;
75
+ if (Number.isInteger(parsed2?.pid) && parsed2.pid > 0 && !processAlive(parsed2.pid)) return null;
64
76
  return { url, token };
65
77
  } catch {
66
78
  return null;
67
79
  }
68
80
  }
69
81
 
82
+ // src/internal/config/dotenv.ts
83
+ var fs2 = __toESM(require("fs"));
84
+
85
+ // ../core/src/dotenv.ts
86
+ function parseDotEnv(text) {
87
+ const out = {};
88
+ for (const raw of text.split(/\r?\n/)) {
89
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
90
+ if (!m) continue;
91
+ let value = m[2];
92
+ const quote = value[0];
93
+ if ((quote === '"' || quote === "'") && value.length > 1 && value.endsWith(quote)) {
94
+ value = value.slice(1, -1);
95
+ if (quote === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
96
+ } else {
97
+ value = value.replace(/\s+#.*$/, "");
98
+ }
99
+ out[m[1]] = value;
100
+ }
101
+ return out;
102
+ }
103
+
104
+ // src/internal/config/dotenv.ts
105
+ function readDotEnv(filePath2) {
106
+ try {
107
+ return parseDotEnv(fs2.readFileSync(filePath2, "utf-8"));
108
+ } catch {
109
+ return {};
110
+ }
111
+ }
112
+
113
+ // src/internal/support/ci.ts
114
+ function detectCi(env) {
115
+ return Boolean(
116
+ env.JENKINS_URL || env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.TRAVIS || env.TF_BUILD || env.BITBUCKET_BUILD_NUMBER || env.TEAMCITY_VERSION || env.CODEBUILD_BUILD_ID || env.bamboo_buildKey || env.CI && env.CI.toLowerCase() !== "false" && env.CI !== "0"
117
+ );
118
+ }
119
+ function detectCiIds(env) {
120
+ if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID) {
121
+ const run = env.GITHUB_RUN_ATTEMPT ? `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` : env.GITHUB_RUN_ID;
122
+ return { run, job: env.GITHUB_JOB || null };
123
+ }
124
+ if (env.GITLAB_CI && env.CI_PIPELINE_ID) return { run: env.CI_PIPELINE_ID, job: env.CI_JOB_ID || null };
125
+ if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID) return { run: env.CIRCLE_WORKFLOW_ID, job: env.CIRCLE_BUILD_NUM || null };
126
+ if (env.TRAVIS && env.TRAVIS_BUILD_ID) return { run: env.TRAVIS_BUILD_ID, job: env.TRAVIS_JOB_ID || null };
127
+ if (env.TF_BUILD && env.BUILD_BUILDID) return { run: env.BUILD_BUILDID, job: env.SYSTEM_JOBID || null };
128
+ if (env.JENKINS_URL && env.BUILD_ID) return { run: env.BUILD_ID, job: null };
129
+ if (env.BUILDKITE_BUILD_ID) return { run: env.BUILDKITE_BUILD_ID, job: env.BUILDKITE_JOB_ID || null };
130
+ if (env.TEAMCITY_BUILD_ID) return { run: env.TEAMCITY_BUILD_ID, job: null };
131
+ if (env.BITBUCKET_BUILD_NUMBER) return { run: env.BITBUCKET_BUILD_NUMBER, job: env.BITBUCKET_STEP_UUID || null };
132
+ if (env.SEMAPHORE_WORKFLOW_ID) return { run: env.SEMAPHORE_WORKFLOW_ID, job: env.SEMAPHORE_JOB_ID || null };
133
+ if (env.APPVEYOR_BUILD_ID) return { run: env.APPVEYOR_BUILD_ID, job: env.APPVEYOR_JOB_ID || null };
134
+ if (env.DRONE_BUILD_NUMBER) return { run: env.DRONE_BUILD_NUMBER, job: null };
135
+ return null;
136
+ }
137
+ function resolveRunLabel(configured, sharded) {
138
+ if (configured) return configured;
139
+ const ci = detectCiIds(process.env);
140
+ if (!ci) return null;
141
+ return !sharded && ci.job ? `${ci.run}|${ci.job}` : ci.run;
142
+ }
143
+
70
144
  // src/internal/config/env.ts
71
145
  var DEFAULTS = {
72
146
  projectName: "default-project",
@@ -103,6 +177,7 @@ var DEFAULTS = {
103
177
  };
104
178
  var PIWI_ENV_KEYS = {
105
179
  serverUrl: "PIWI_DASHBOARD_URL",
180
+ desktopApp: "PIWI_DESKTOP_APP",
106
181
  projectName: "PIWI_PROJECT_NAME",
107
182
  verbose: "PIWI_VERBOSE",
108
183
  apiKey: "PIWI_API_KEY",
@@ -215,11 +290,27 @@ var ENV_FALLBACK_SPECS = [
215
290
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
216
291
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
217
292
  ];
293
+ var DOTENV_OPTIONS = ["serverUrl", "apiKey", "projectName"];
294
+ var DESKTOP_APP_MODES = ["prefer", "fallback", "off"];
295
+ function desktopAppMode(option, envValue) {
296
+ const fromEnv = envValue?.trim().toLowerCase();
297
+ if (DESKTOP_APP_MODES.includes(fromEnv)) return fromEnv;
298
+ if (DESKTOP_APP_MODES.includes(option)) return option;
299
+ return "fallback";
300
+ }
218
301
  var desktopDiscovered = false;
302
+ var desktopReplacedServerUrl = null;
303
+ var dotEnvUse = null;
219
304
  function usedDesktopDiscovery() {
220
305
  return desktopDiscovered;
221
306
  }
222
- function resolveOptions(raw) {
307
+ function desktopReplacedServer() {
308
+ return desktopReplacedServerUrl;
309
+ }
310
+ function usedDotEnv() {
311
+ return dotEnvUse;
312
+ }
313
+ function resolveOptions(raw, configDir) {
223
314
  const env = process.env;
224
315
  const mergedRaw = { ...raw };
225
316
  for (const spec of ENV_FALLBACK_SPECS) {
@@ -231,14 +322,35 @@ function resolveOptions(raw) {
231
322
  mergedRaw[spec.option] = spec.kind === "number" ? Number(value) : value;
232
323
  }
233
324
  }
325
+ dotEnvUse = null;
326
+ if (configDir) {
327
+ const file = path2.join(configDir, ".env");
328
+ const dotEnv = readDotEnv(file);
329
+ const keys = [];
330
+ for (const option of DOTENV_OPTIONS) {
331
+ const value = dotEnv[PIWI_ENV_KEYS[option]];
332
+ if (mergedRaw[option] !== void 0 || !value) continue;
333
+ mergedRaw[option] = value;
334
+ keys.push(PIWI_ENV_KEYS[option]);
335
+ }
336
+ if (keys.length > 0) dotEnvUse = { file, keys };
337
+ }
234
338
  desktopDiscovered = false;
339
+ desktopReplacedServerUrl = null;
340
+ const mode = desktopAppMode(mergedRaw.desktopApp, env[PIWI_ENV_KEYS.desktopApp]);
341
+ mergedRaw.desktopApp = mode;
235
342
  const serverUrlUnset = mergedRaw.serverUrl === void 0;
236
343
  const apiKeyUnset = mergedRaw.apiKey === void 0 || mergedRaw.apiKey === null;
237
- if (serverUrlUnset && apiKeyUnset) {
344
+ const nothingConfigured = serverUrlUnset && apiKeyUnset;
345
+ const preferLocally = mode === "prefer" && !detectCi(env);
346
+ if (mode !== "off" && (nothingConfigured || preferLocally)) {
238
347
  const desktop = readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath());
239
348
  if (desktop) {
349
+ if (!serverUrlUnset && mergedRaw.serverUrl !== desktop.url) desktopReplacedServerUrl = mergedRaw.serverUrl;
240
350
  mergedRaw.serverUrl = desktop.url;
241
351
  mergedRaw.apiKey = desktop.token;
352
+ mergedRaw.username = null;
353
+ mergedRaw.password = null;
242
354
  desktopDiscovered = true;
243
355
  }
244
356
  }
@@ -263,6 +375,8 @@ function resolveOptions(raw) {
263
375
  function applyOptionsToEnv(options) {
264
376
  const env = process.env;
265
377
  if (options.serverUrl !== void 0) env[PIWI_ENV_KEYS.serverUrl] = options.serverUrl;
378
+ if (options.desktopApp !== void 0 && !env[PIWI_ENV_KEYS.desktopApp])
379
+ env[PIWI_ENV_KEYS.desktopApp] = options.desktopApp;
266
380
  if (options.projectName !== void 0) env[PIWI_ENV_KEYS.projectName] = options.projectName;
267
381
  if (options.verbose !== void 0) env[PIWI_ENV_KEYS.verbose] = String(options.verbose);
268
382
  if (options.apiKey) env[PIWI_ENV_KEYS.apiKey] = options.apiKey;
@@ -545,7 +659,7 @@ var HttpClient = class {
545
659
  }
546
660
  /** Unified request core: transport, headers, auth, response accumulation, timeout. */
547
661
  request(method, pathname, opts) {
548
- return new Promise((resolve9, reject) => {
662
+ return new Promise((resolve11, reject) => {
549
663
  if (this.closedReason !== null) {
550
664
  reject(new Error(this.closedReason));
551
665
  return;
@@ -568,7 +682,7 @@ var HttpClient = class {
568
682
  data += chunk;
569
683
  });
570
684
  res.on("end", () => {
571
- resolve9({ status: res.statusCode ?? 0, text: data, headers: res.headers });
685
+ resolve11({ status: res.statusCode ?? 0, text: data, headers: res.headers });
572
686
  });
573
687
  res.on("error", reject);
574
688
  res.on("close", () => {
@@ -607,8 +721,8 @@ var HttpClient = class {
607
721
  };
608
722
 
609
723
  // src/internal/submit/uploader.ts
610
- var fs2 = __toESM(require("fs"));
611
- var path2 = __toESM(require("path"));
724
+ var fs3 = __toESM(require("fs"));
725
+ var path3 = __toESM(require("path"));
612
726
  var import_form_data2 = __toESM(require("form-data"));
613
727
 
614
728
  // src/internal/submit/serializer.ts
@@ -634,6 +748,7 @@ function toWireTestCase(tc) {
634
748
  timeout: rest.timeout ?? null,
635
749
  error: rest.error,
636
750
  retries: rest.retries,
751
+ repeatEachIndex: rest.repeatEachIndex ?? 0,
637
752
  attempts: rest.attempts || null,
638
753
  workerIndex: rest.workerIndex ?? null,
639
754
  shardIndex: rest.shardIndex ?? null,
@@ -686,7 +801,13 @@ function isFailingStatus(status) {
686
801
  function computeDistinctRunCounts(testCases) {
687
802
  const final = /* @__PURE__ */ new Map();
688
803
  for (const tc of testCases) {
689
- const key = tc.testId ?? [tc.title, tc.location, (tc.suitePath ?? []).join(""), browserProjectName(tc.browser)].join("\0");
804
+ const key = tc.testId ?? [
805
+ tc.title,
806
+ tc.location,
807
+ (tc.suitePath ?? []).join(""),
808
+ browserProjectName(tc.browser),
809
+ tc.repeatEachIndex ?? 0
810
+ ].join("\0");
690
811
  const status = tc.status ?? "";
691
812
  const retries = tc.retries ?? 0;
692
813
  const prev = final.get(key);
@@ -844,14 +965,15 @@ var Uploader = class {
844
965
  title: testCase.title,
845
966
  location: testCase.location,
846
967
  retries: testCase.retries ?? 0,
968
+ repeatEachIndex: testCase.repeatEachIndex ?? 0,
847
969
  suitePath: testCase.suitePath ?? null
848
970
  })
849
971
  );
850
972
  if (traceInfo) {
851
973
  form.append("trace_hash", traceInfo.hash);
852
974
  if (includeTraceFile2) {
853
- form.append("trace", fs2.createReadStream(traceInfo.tracePath), {
854
- filename: path2.basename(traceInfo.tracePath)
975
+ form.append("trace", fs3.createReadStream(traceInfo.tracePath), {
976
+ filename: path3.basename(traceInfo.tracePath)
855
977
  });
856
978
  }
857
979
  }
@@ -863,7 +985,7 @@ var Uploader = class {
863
985
  )
864
986
  );
865
987
  for (const a of attachments) {
866
- form.append("attach_file", fs2.createReadStream(a.path), { filename: a.originalName });
988
+ form.append("attach_file", fs3.createReadStream(a.path), { filename: a.originalName });
867
989
  }
868
990
  }
869
991
  return form;
@@ -916,7 +1038,7 @@ var Uploader = class {
916
1038
  )
917
1039
  );
918
1040
  for (const a of attachments) {
919
- form.append(`attach_file_${i}`, fs2.createReadStream(a.path), { filename: a.originalName });
1041
+ form.append(`attach_file_${i}`, fs3.createReadStream(a.path), { filename: a.originalName });
920
1042
  attachmentCount++;
921
1043
  }
922
1044
  }
@@ -925,8 +1047,8 @@ var Uploader = class {
925
1047
  let traceCount = 0;
926
1048
  for (const [i, tc] of testCases.entries()) {
927
1049
  for (const tp of this.fileHandler.findTraceFiles(tc)) {
928
- if (fs2.existsSync(tp)) {
929
- form.append(`trace_${i}`, fs2.createReadStream(tp), { filename: path2.basename(tp) });
1050
+ if (fs3.existsSync(tp)) {
1051
+ form.append(`trace_${i}`, fs3.createReadStream(tp), { filename: path3.basename(tp) });
930
1052
  traceCount++;
931
1053
  }
932
1054
  }
@@ -936,9 +1058,9 @@ var Uploader = class {
936
1058
  };
937
1059
 
938
1060
  // src/internal/streaming/stream-buffer.ts
939
- var path3 = __toESM(require("path"));
1061
+ var path4 = __toESM(require("path"));
940
1062
  var os3 = __toESM(require("os"));
941
- var fs3 = __toESM(require("fs"));
1063
+ var fs4 = __toESM(require("fs"));
942
1064
 
943
1065
  // src/internal/support/instance-id.ts
944
1066
  var crypto = __toESM(require("crypto"));
@@ -959,14 +1081,14 @@ var StreamBuffer = class {
959
1081
  }
960
1082
  /** Scope the buffer to a run. Until then the buffer is empty and appends are dropped. */
961
1083
  bindRun(runId) {
962
- this.filePath = path3.join(os3.tmpdir(), `${this.prefix}-${runId}.jsonl`);
1084
+ this.filePath = path4.join(os3.tmpdir(), `${this.prefix}-${runId}.jsonl`);
963
1085
  }
964
1086
  /** Append one or more events to the on-disk buffer */
965
1087
  append(events) {
966
1088
  if (!this.filePath || events.length === 0) return;
967
1089
  try {
968
1090
  const lines = events.map((e) => JSON.stringify(e) + "\n").join("");
969
- fs3.appendFileSync(this.filePath, lines, "utf8");
1091
+ fs4.appendFileSync(this.filePath, lines, "utf8");
970
1092
  } catch {
971
1093
  }
972
1094
  }
@@ -974,8 +1096,8 @@ var StreamBuffer = class {
974
1096
  load() {
975
1097
  if (!this.filePath) return [];
976
1098
  try {
977
- if (fs3.existsSync(this.filePath)) {
978
- const content = fs3.readFileSync(this.filePath, "utf8");
1099
+ if (fs4.existsSync(this.filePath)) {
1100
+ const content = fs4.readFileSync(this.filePath, "utf8");
979
1101
  return content.split("\n").filter(Boolean).map((line) => JSON.parse(line));
980
1102
  }
981
1103
  } catch {
@@ -986,7 +1108,7 @@ var StreamBuffer = class {
986
1108
  clear() {
987
1109
  if (!this.filePath) return;
988
1110
  try {
989
- if (fs3.existsSync(this.filePath)) fs3.unlinkSync(this.filePath);
1111
+ if (fs4.existsSync(this.filePath)) fs4.unlinkSync(this.filePath);
990
1112
  } catch {
991
1113
  }
992
1114
  }
@@ -995,14 +1117,14 @@ var StreamBuffer = class {
995
1117
  const dir = os3.tmpdir();
996
1118
  let names;
997
1119
  try {
998
- names = fs3.readdirSync(dir).filter((name) => name.startsWith(this.prefix));
1120
+ names = fs4.readdirSync(dir).filter((name) => name.startsWith(this.prefix));
999
1121
  } catch {
1000
1122
  return;
1001
1123
  }
1002
1124
  for (const name of names) {
1003
1125
  try {
1004
- const file = path3.join(dir, name);
1005
- if (Date.now() - fs3.statSync(file).mtimeMs > maxAgeMs) fs3.unlinkSync(file);
1126
+ const file = path4.join(dir, name);
1127
+ if (Date.now() - fs4.statSync(file).mtimeMs > maxAgeMs) fs4.unlinkSync(file);
1006
1128
  } catch {
1007
1129
  }
1008
1130
  }
@@ -1011,9 +1133,9 @@ var StreamBuffer = class {
1011
1133
 
1012
1134
  // src/internal/streaming/crash-recovery.ts
1013
1135
  var crypto2 = __toESM(require("crypto"));
1014
- var fs4 = __toESM(require("fs"));
1136
+ var fs5 = __toESM(require("fs"));
1015
1137
  var os4 = __toESM(require("os"));
1016
- var path4 = __toESM(require("path"));
1138
+ var path5 = __toESM(require("path"));
1017
1139
 
1018
1140
  // src/internal/support/errors.ts
1019
1141
  function errorMessage(error) {
@@ -1028,7 +1150,7 @@ function recoveryServer(serverUrl) {
1028
1150
  function recoveryFilePath(serverUrl, projectName) {
1029
1151
  const key = crypto2.createHash("sha256").update(`${recoveryServer(serverUrl)}
1030
1152
  ${projectName}`).digest("hex").slice(0, 16);
1031
- return path4.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
1153
+ return path5.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
1032
1154
  }
1033
1155
  function refusesRun(error) {
1034
1156
  return error instanceof HttpError && [400, 409, 422].includes(error.status);
@@ -1049,11 +1171,11 @@ var CrashRecovery = class {
1049
1171
  const file = { v: 1, serverUrl: this.serverUrl, payload };
1050
1172
  const temp = `${this.filePath}.${process.pid}.${crypto2.randomBytes(6).toString("hex")}.tmp`;
1051
1173
  try {
1052
- fs4.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
1053
- fs4.renameSync(temp, this.filePath);
1174
+ fs5.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
1175
+ fs5.renameSync(temp, this.filePath);
1054
1176
  this.logger.info("Saved recovery data for later upload");
1055
1177
  } catch (error) {
1056
- fs4.rmSync(temp, { force: true });
1178
+ fs5.rmSync(temp, { force: true });
1057
1179
  this.logger.error(`Failed to save recovery data: ${errorMessage(error)}`);
1058
1180
  }
1059
1181
  }
@@ -1064,26 +1186,26 @@ var CrashRecovery = class {
1064
1186
  load() {
1065
1187
  let fd;
1066
1188
  try {
1067
- fd = fs4.openSync(this.filePath, fs4.constants.O_RDONLY | (fs4.constants.O_NOFOLLOW ?? 0));
1189
+ fd = fs5.openSync(this.filePath, fs5.constants.O_RDONLY | (fs5.constants.O_NOFOLLOW ?? 0));
1068
1190
  } catch {
1069
1191
  return null;
1070
1192
  }
1071
1193
  try {
1072
- const stat = fs4.fstatSync(fd);
1194
+ const stat = fs5.fstatSync(fd);
1073
1195
  if (!stat.isFile() || typeof process.getuid === "function" && stat.uid !== process.getuid()) return null;
1074
- const file = JSON.parse(fs4.readFileSync(fd, "utf8"));
1196
+ const file = JSON.parse(fs5.readFileSync(fd, "utf8"));
1075
1197
  if (file?.v !== 1 || file.serverUrl !== this.serverUrl) return null;
1076
1198
  return file.payload && typeof file.payload === "object" ? file.payload : null;
1077
1199
  } catch {
1078
1200
  return null;
1079
1201
  } finally {
1080
- fs4.closeSync(fd);
1202
+ fs5.closeSync(fd);
1081
1203
  }
1082
1204
  }
1083
1205
  /** Delete the recovery file from disk */
1084
1206
  clear() {
1085
1207
  try {
1086
- fs4.rmSync(this.filePath, { force: true });
1208
+ fs5.rmSync(this.filePath, { force: true });
1087
1209
  } catch {
1088
1210
  }
1089
1211
  }
@@ -1115,25 +1237,25 @@ var CrashRecovery = class {
1115
1237
  };
1116
1238
 
1117
1239
  // src/internal/files/file-handler.ts
1118
- var fs6 = __toESM(require("fs"));
1240
+ var fs7 = __toESM(require("fs"));
1119
1241
  var os5 = __toESM(require("os"));
1120
- var path6 = __toESM(require("path"));
1242
+ var path7 = __toESM(require("path"));
1121
1243
  var crypto3 = __toESM(require("crypto"));
1122
1244
 
1123
1245
  // src/internal/files/compression.ts
1124
- var fs5 = __toESM(require("fs"));
1125
- var path5 = __toESM(require("path"));
1246
+ var fs6 = __toESM(require("fs"));
1247
+ var path6 = __toESM(require("path"));
1126
1248
  var zlib = __toESM(require("zlib"));
1127
1249
  var import_node_util = require("util");
1128
1250
  var gzipAsync = (0, import_node_util.promisify)(zlib.gzip);
1129
1251
  async function compressDirectory(sourceDir) {
1130
1252
  const files = [];
1131
1253
  function collect(dir, baseDir = "") {
1132
- for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
1133
- const full = path5.join(dir, entry.name);
1134
- const rel = path5.join(baseDir, entry.name);
1254
+ for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
1255
+ const full = path6.join(dir, entry.name);
1256
+ const rel = path6.join(baseDir, entry.name);
1135
1257
  if (entry.isDirectory()) collect(full, rel);
1136
- else if (entry.isFile()) files.push({ path: rel, content: fs5.readFileSync(full) });
1258
+ else if (entry.isFile()) files.push({ path: rel, content: fs6.readFileSync(full) });
1137
1259
  }
1138
1260
  }
1139
1261
  collect(sourceDir);
@@ -1197,7 +1319,8 @@ var ATTACHMENT_NAMES = {
1197
1319
  userPick: "piwi-user-pick",
1198
1320
  aiUsage: "piwi-ai-usage",
1199
1321
  aiMeta: "piwi-ai-meta",
1200
- resources: "piwi-resources"
1322
+ resources: "piwi-resources",
1323
+ pauses: "piwi-pauses"
1201
1324
  };
1202
1325
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
1203
1326
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
@@ -1231,19 +1354,19 @@ var FileHandler = class {
1231
1354
  }
1232
1355
  /** Locate a Playwright HTML report directory containing `index.html`. Optionally override the search path. */
1233
1356
  findHTMLReportDirectory(customDir) {
1234
- const possibleDirs = customDir ? [customDir, path6.join(process.cwd(), customDir)] : ["playwright-report", "./playwright-report", path6.join(process.cwd(), "playwright-report")];
1357
+ const possibleDirs = customDir ? [customDir, path7.join(process.cwd(), customDir)] : ["playwright-report", "./playwright-report", path7.join(process.cwd(), "playwright-report")];
1235
1358
  for (const reportDir of possibleDirs) {
1236
- if (fs6.existsSync(reportDir) && fs6.statSync(reportDir).isDirectory()) {
1237
- if (fs6.existsSync(path6.join(reportDir, "index.html"))) return reportDir;
1359
+ if (fs7.existsSync(reportDir) && fs7.statSync(reportDir).isDirectory()) {
1360
+ if (fs7.existsSync(path7.join(reportDir, "index.html"))) return reportDir;
1238
1361
  }
1239
1362
  }
1240
1363
  return null;
1241
1364
  }
1242
1365
  /** Check if `dir` exists and is a directory; also checks `path.join(process.cwd(), dir)` */
1243
1366
  findReportDirectory(dir) {
1244
- const candidates = [dir, path6.join(process.cwd(), dir)];
1367
+ const candidates = [dir, path7.join(process.cwd(), dir)];
1245
1368
  for (const candidate of candidates) {
1246
- if (fs6.existsSync(candidate) && fs6.statSync(candidate).isDirectory()) return candidate;
1369
+ if (fs7.existsSync(candidate) && fs7.statSync(candidate).isDirectory()) return candidate;
1247
1370
  }
1248
1371
  return null;
1249
1372
  }
@@ -1261,7 +1384,7 @@ var FileHandler = class {
1261
1384
  const set = /* @__PURE__ */ new Set();
1262
1385
  if (testCase.attachments) {
1263
1386
  for (const a of testCase.attachments) {
1264
- if (a.name === "trace" && a.path) set.add(path6.resolve(a.path));
1387
+ if (a.name === "trace" && a.path) set.add(path7.resolve(a.path));
1265
1388
  }
1266
1389
  }
1267
1390
  return Array.from(set);
@@ -1289,24 +1412,24 @@ var FileHandler = class {
1289
1412
  cleanupBodyAttachments() {
1290
1413
  if (!this.bodyDir) return;
1291
1414
  try {
1292
- fs6.rmSync(this.bodyDir, { recursive: true, force: true });
1415
+ fs7.rmSync(this.bodyDir, { recursive: true, force: true });
1293
1416
  } catch (error) {
1294
1417
  this.logger.debug(`Could not remove temp attachments at ${this.bodyDir}: ${errorMessage(error)}`);
1295
1418
  }
1296
1419
  this.bodyDir = null;
1297
1420
  }
1298
1421
  resolvePathAttachment(a, testCase) {
1299
- if (!a.path || !fs6.existsSync(a.path)) return null;
1300
- const size = fs6.statSync(a.path).size;
1422
+ if (!a.path || !fs7.existsSync(a.path)) return null;
1423
+ const size = fs7.statSync(a.path).size;
1301
1424
  if (size > this.maxAttachmentBytes) {
1302
1425
  this.warnOversized(a, testCase, size);
1303
1426
  return null;
1304
1427
  }
1305
1428
  return {
1306
1429
  name: a.name || "attachment",
1307
- path: path6.resolve(a.path),
1430
+ path: path7.resolve(a.path),
1308
1431
  contentType: a.contentType || "application/octet-stream",
1309
- originalName: path6.basename(a.path)
1432
+ originalName: path7.basename(a.path)
1310
1433
  };
1311
1434
  }
1312
1435
  resolveBodyAttachment(a, testCase) {
@@ -1321,8 +1444,8 @@ var FileHandler = class {
1321
1444
  let filePath2 = this.bodyFiles.get(a);
1322
1445
  if (!filePath2) {
1323
1446
  try {
1324
- filePath2 = path6.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
1325
- fs6.writeFileSync(filePath2, body);
1447
+ filePath2 = path7.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
1448
+ fs7.writeFileSync(filePath2, body);
1326
1449
  this.bodyFiles.set(a, filePath2);
1327
1450
  } catch (error) {
1328
1451
  this.logger.warn(`Could not stage attachment "${a.name}" for upload: ${errorMessage(error)}`);
@@ -1332,7 +1455,7 @@ var FileHandler = class {
1332
1455
  return { name: a.name || "attachment", path: filePath2, contentType, originalName };
1333
1456
  }
1334
1457
  bodyAttachmentDir() {
1335
- if (!this.bodyDir) this.bodyDir = fs6.mkdtempSync(path6.join(os5.tmpdir(), "piwi-dashboard-attachments-"));
1458
+ if (!this.bodyDir) this.bodyDir = fs7.mkdtempSync(path7.join(os5.tmpdir(), "piwi-dashboard-attachments-"));
1336
1459
  return this.bodyDir;
1337
1460
  }
1338
1461
  warnOversized(a, testCase, size) {
@@ -1355,10 +1478,10 @@ var FileHandler = class {
1355
1478
  /** Parse Piwi-internal attachment bodies (`piwi-network`, `piwi-web-vitals`, `piwi-console`, `piwi-aria-snapshot`, `piwi-aria-snapshot-json`) into structured fields on the test case */
1356
1479
  parsePerformanceAttachments(testCase, attachments) {
1357
1480
  const find = (name) => attachments.find((a) => a.name === name);
1358
- const net = find(ATTACHMENT_NAMES.network);
1359
- if (net?.body) {
1481
+ const net2 = find(ATTACHMENT_NAMES.network);
1482
+ if (net2?.body) {
1360
1483
  try {
1361
- testCase.networkRequests = JSON.parse(net.body.toString());
1484
+ testCase.networkRequests = JSON.parse(net2.body.toString());
1362
1485
  } catch {
1363
1486
  }
1364
1487
  }
@@ -1414,17 +1537,17 @@ var FileHandler = class {
1414
1537
  const tracePaths = this.findTraceFiles(testCase);
1415
1538
  let lastPath = null;
1416
1539
  for (const tp of tracePaths) {
1417
- if (fs6.existsSync(tp)) lastPath = tp;
1540
+ if (fs7.existsSync(tp)) lastPath = tp;
1418
1541
  }
1419
1542
  if (!lastPath) return null;
1420
1543
  const hash = crypto3.createHash("sha256");
1421
- await new Promise((resolve9, reject) => {
1422
- fs6.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve9).on("error", reject);
1544
+ await new Promise((resolve11, reject) => {
1545
+ fs7.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve11).on("error", reject);
1423
1546
  });
1424
1547
  return {
1425
1548
  tracePath: lastPath,
1426
1549
  hash: hash.digest("hex"),
1427
- size: fs6.statSync(lastPath).size
1550
+ size: fs7.statSync(lastPath).size
1428
1551
  };
1429
1552
  }
1430
1553
  /** Ask the server which trace hashes it already has, returning the set of hashes that are missing */
@@ -1451,14 +1574,14 @@ function formatMiB(bytes) {
1451
1574
  var import_node_child_process = require("child_process");
1452
1575
 
1453
1576
  // src/internal/support/test-paths.ts
1454
- var path7 = __toESM(require("path"));
1577
+ var path8 = __toESM(require("path"));
1455
1578
  function configDirFromConfig(config) {
1456
- if (config?.configFile) return path7.dirname(config.configFile);
1579
+ if (config?.configFile) return path8.dirname(config.configFile);
1457
1580
  if (config?.rootDir) return config.rootDir;
1458
1581
  return process.cwd();
1459
1582
  }
1460
1583
  function relativeTestPath(file, base) {
1461
- return path7.relative(base, file).split(path7.sep).join("/");
1584
+ return path8.relative(base, file).split(path8.sep).join("/");
1462
1585
  }
1463
1586
 
1464
1587
  // src/internal/collect/metadata-collector.ts
@@ -1518,11 +1641,6 @@ function resolveScmBaseBranch(env) {
1518
1641
  const trimmed = target?.trim();
1519
1642
  return trimmed ? normalizeRef(trimmed) : void 0;
1520
1643
  }
1521
- function detectCi(env) {
1522
- return Boolean(
1523
- env.JENKINS_URL || env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.TRAVIS || env.TF_BUILD || env.BITBUCKET_BUILD_NUMBER || env.CI && env.CI !== "false" && env.CI !== "0"
1524
- );
1525
- }
1526
1644
  function resolveRunOrigin(env, warn) {
1527
1645
  const raw = env[PIWI_ORIGIN_ENV.kind]?.trim();
1528
1646
  let kind = detectCi(env) ? "ci" : "local";
@@ -1761,7 +1879,7 @@ var MetadataCollector = class {
1761
1879
  };
1762
1880
 
1763
1881
  // src/internal/streaming/stream-manager.ts
1764
- var fs8 = __toESM(require("fs"));
1882
+ var fs9 = __toESM(require("fs"));
1765
1883
 
1766
1884
  // src/internal/streaming/bounded-queue.ts
1767
1885
  var DROP_TIER = {
@@ -1900,10 +2018,10 @@ function createLimiter(maxConcurrent) {
1900
2018
  if (run) run();
1901
2019
  };
1902
2020
  return function limit(fn) {
1903
- return new Promise((resolve9, reject) => {
2021
+ return new Promise((resolve11, reject) => {
1904
2022
  const run = () => {
1905
2023
  active++;
1906
- Promise.resolve().then(fn).then(resolve9, reject).finally(next);
2024
+ Promise.resolve().then(fn).then(resolve11, reject).finally(next);
1907
2025
  };
1908
2026
  if (active < limitValue) run();
1909
2027
  else queue.push(run);
@@ -1912,18 +2030,18 @@ function createLimiter(maxConcurrent) {
1912
2030
  }
1913
2031
 
1914
2032
  // src/internal/support/setup-file.ts
1915
- var path8 = __toESM(require("path"));
2033
+ var path9 = __toESM(require("path"));
1916
2034
  var os6 = __toESM(require("os"));
1917
- var fs7 = __toESM(require("fs"));
2035
+ var fs8 = __toESM(require("fs"));
1918
2036
  function getSetupFilePath(projectName) {
1919
- return path8.join(os6.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
2037
+ return path9.join(os6.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
1920
2038
  }
1921
2039
  function readSetupInfo(projectName) {
1922
2040
  const setupFile = getSetupFilePath(projectName);
1923
2041
  try {
1924
- if (fs7.existsSync(setupFile)) {
1925
- const info = JSON.parse(fs7.readFileSync(setupFile, "utf8"));
1926
- fs7.unlinkSync(setupFile);
2042
+ if (fs8.existsSync(setupFile)) {
2043
+ const info = JSON.parse(fs8.readFileSync(setupFile, "utf8"));
2044
+ fs8.unlinkSync(setupFile);
1927
2045
  if (info.projectName === projectName) return info;
1928
2046
  }
1929
2047
  } catch {
@@ -2142,7 +2260,7 @@ var StreamManager = class {
2142
2260
  } catch (error) {
2143
2261
  if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
2144
2262
  this.logger.warn(
2145
- `Live streaming could not start: the dashboard at ${this.options.serverUrl} requires authentication. Set the reporter's \`apiKey\` option (or PIWI_API_KEY) \u2014 create a key under Settings \u2192 Users on the dashboard. Falling back to batch upload at the end of the run.`
2263
+ `Live streaming could not start: the dashboard at ${this.options.serverUrl} requires authentication. Set the reporter's \`apiKey\` option, or PIWI_API_KEY in the environment or in the .env file next to the Playwright config \u2014 create a key under Settings \u2192 Users on the dashboard. Falling back to batch upload at the end of the run.`
2146
2264
  );
2147
2265
  } else {
2148
2266
  this.logger.warn(
@@ -2379,7 +2497,7 @@ var StreamManager = class {
2379
2497
  this.logger.debugError(
2380
2498
  `${this.pendingEvents.length} events pending, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`
2381
2499
  );
2382
- await new Promise((resolve9) => setTimeout(resolve9, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
2500
+ await new Promise((resolve11) => setTimeout(resolve11, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
2383
2501
  }
2384
2502
  if (!this.pendingEvents.isEmpty) {
2385
2503
  const remaining = this.pendingEvents.takeAll();
@@ -2423,7 +2541,7 @@ var StreamManager = class {
2423
2541
  }
2424
2542
  /** Schedule a live upload of trace and attachment files for a test case. Skips cases with no files. Concurrency is limited to 2 simultaneous uploads. */
2425
2543
  scheduleLiveUpload(tc) {
2426
- const hasTrace = !!this.options.uploadTraces && this.fileHandler.findTraceFiles(tc).some((p) => fs8.existsSync(p));
2544
+ const hasTrace = !!this.options.uploadTraces && this.fileHandler.findTraceFiles(tc).some((p) => fs9.existsSync(p));
2427
2545
  const hasAttachments = this.fileHandler.findAllAttachments(tc).length > 0;
2428
2546
  if (!hasTrace && !hasAttachments) return;
2429
2547
  const promise = (async () => {
@@ -2436,7 +2554,7 @@ var StreamManager = class {
2436
2554
  if (flush) await flush;
2437
2555
  const delays = [0, 1e3, 3e3];
2438
2556
  for (let attempt = 0; attempt < delays.length; attempt++) {
2439
- if (delays[attempt]) await new Promise((resolve9) => setTimeout(resolve9, delays[attempt]));
2557
+ if (delays[attempt]) await new Promise((resolve11) => setTimeout(resolve11, delays[attempt]));
2440
2558
  try {
2441
2559
  await this.limitLiveUpload(
2442
2560
  () => this.uploader.uploadCaseFiles(
@@ -3251,7 +3369,7 @@ function maskUrlFragment(hash) {
3251
3369
  }
3252
3370
 
3253
3371
  // ../core/src/locator-break.ts
3254
- var filePath = (path32) => path32.replace(/\\/g, "/").replace(/^\.\//, "");
3372
+ var filePath = (path34) => path34.replace(/\\/g, "/").replace(/^\.\//, "");
3255
3373
  function sameFilePath(a, b) {
3256
3374
  const x = filePath(a);
3257
3375
  const y = filePath(b);
@@ -3546,23 +3664,48 @@ function flattenSteps(steps, depth = 0) {
3546
3664
  }
3547
3665
  return result;
3548
3666
  }
3549
- function collectStepMetrics(steps, errors) {
3667
+ function epochMs(value) {
3668
+ if (value instanceof Date) return value.getTime();
3669
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
3670
+ }
3671
+ function pausedOverlap(start, duration, pauses) {
3672
+ const from = epochMs(start);
3673
+ const length = typeof duration === "number" && duration > 0 ? duration : 0;
3674
+ if (from === null || length === 0 || pauses.length === 0) return 0;
3675
+ const to = from + length;
3676
+ const spans = pauses.filter((p) => p.duration > 0).map((p) => [Math.max(from, p.startedAt), Math.min(to, p.startedAt + p.duration)]).filter(([a, b]) => b > a).sort((x, y) => x[0] - y[0]);
3677
+ let ms = 0;
3678
+ let reached = from;
3679
+ for (const [a, b] of spans) {
3680
+ const begin = Math.max(a, reached);
3681
+ if (b > begin) ms += b - begin;
3682
+ reached = Math.max(reached, b);
3683
+ }
3684
+ return ms;
3685
+ }
3686
+ function unpausedDuration(step, pauses) {
3687
+ const duration = typeof step.duration === "number" && step.duration > 0 ? step.duration : 0;
3688
+ return duration - pausedOverlap(step.startTime, duration, pauses);
3689
+ }
3690
+ function collectStepMetrics(steps, errors, pauses = []) {
3550
3691
  const flatSteps = flattenSteps(steps);
3551
3692
  if (errors) markRecoveredSteps(flatSteps, errors);
3552
- const totalStepDuration = steps.reduce((sum, s) => sum + (s.duration || 0), 0);
3693
+ const own = (s) => pauses.length > 0 && s.duration > 0 ? unpausedDuration(s, pauses) : s.duration;
3694
+ const totalStepDuration = steps.reduce((sum, s) => sum + (own(s) || 0), 0);
3553
3695
  let slowestStep = null;
3554
3696
  for (const s of flatSteps) {
3555
- if (!slowestStep || s.duration > slowestStep.duration) slowestStep = { title: s.title, duration: s.duration };
3697
+ const duration = own(s);
3698
+ if (!slowestStep || duration > slowestStep.duration) slowestStep = { title: s.title, duration };
3556
3699
  }
3557
3700
  const navSteps = flatSteps.filter((s) => s.category === "navigation");
3558
3701
  const waitSteps = flatSteps.filter((s) => s.category === "wait");
3559
- const waitTotalDuration = waitSteps.reduce((sum, s) => sum + (s.duration || 0), 0);
3702
+ const waitTotalDuration = waitSteps.reduce((sum, s) => sum + (own(s) || 0), 0);
3560
3703
  return {
3561
3704
  steps: flatSteps,
3562
3705
  totalStepDuration,
3563
3706
  slowestStep,
3564
3707
  navigationCount: navSteps.length,
3565
- navigationTotalDuration: navSteps.reduce((sum, s) => sum + (s.duration || 0), 0),
3708
+ navigationTotalDuration: navSteps.reduce((sum, s) => sum + (own(s) || 0), 0),
3566
3709
  waitTotalDuration,
3567
3710
  waitCount: waitSteps.length
3568
3711
  };
@@ -3678,19 +3821,43 @@ function extractWaitEvents(steps, insideWait = false) {
3678
3821
  return events;
3679
3822
  }
3680
3823
 
3824
+ // src/internal/collect/pause-events.ts
3825
+ function readPauses(body) {
3826
+ if (!body) return [];
3827
+ try {
3828
+ const value = JSON.parse(body.toString("utf8"));
3829
+ if (!Array.isArray(value)) return [];
3830
+ return value.filter(
3831
+ (p) => !!p && typeof p === "object" && typeof p.location === "string" && typeof p.startedAt === "number" && typeof p.duration === "number"
3832
+ );
3833
+ } catch {
3834
+ return [];
3835
+ }
3836
+ }
3837
+ function pauseStepEvents(pauses) {
3838
+ return pauses.map((pause) => ({
3839
+ title: typeof pause.title === "string" ? pause.title : "",
3840
+ category: "pause",
3841
+ startedAt: pause.startedAt,
3842
+ duration: Math.max(0, pause.duration),
3843
+ status: typeof pause.reason === "string" ? pause.reason : "breakpoint",
3844
+ location: pause.location
3845
+ }));
3846
+ }
3847
+
3681
3848
  // src/internal/support/reporter-version.ts
3682
- var fs9 = __toESM(require("fs"));
3683
- var path9 = __toESM(require("path"));
3849
+ var fs10 = __toESM(require("fs"));
3850
+ var path10 = __toESM(require("path"));
3684
3851
  var cachedVersion = null;
3685
3852
  var CANDIDATE_OFFSETS = ["..", "../../.."];
3686
3853
  function findOwnPackageJson(fromDir) {
3687
3854
  for (const offset of CANDIDATE_OFFSETS) {
3688
3855
  try {
3689
- const pkgPath = path9.resolve(fromDir, offset, "package.json");
3690
- const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
3856
+ const pkgPath = path10.resolve(fromDir, offset, "package.json");
3857
+ const pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
3691
3858
  const { name, version } = pkg;
3692
3859
  if (name === "@piwitests/reporter" && typeof version === "string")
3693
- return { version, root: path9.dirname(pkgPath) };
3860
+ return { version, root: path10.dirname(pkgPath) };
3694
3861
  } catch {
3695
3862
  }
3696
3863
  }
@@ -3703,11 +3870,11 @@ function getReporterVersion() {
3703
3870
  }
3704
3871
 
3705
3872
  // src/internal/support/source-snippet.ts
3706
- var fs10 = __toESM(require("fs"));
3707
- var path10 = __toESM(require("path"));
3873
+ var fs11 = __toESM(require("fs"));
3874
+ var path11 = __toESM(require("path"));
3708
3875
  function readSourceSnippet(file, declLine, context, failingLine) {
3709
3876
  try {
3710
- const content = fs10.readFileSync(file, "utf-8");
3877
+ const content = fs11.readFileSync(file, "utf-8");
3711
3878
  const lines = content.split("\n");
3712
3879
  const anchor = failingLine ?? declLine;
3713
3880
  const start = Math.max(0, anchor - context - 1);
@@ -3747,16 +3914,16 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
3747
3914
  if (picked.length >= maxFrames) break;
3748
3915
  let abs;
3749
3916
  try {
3750
- abs = path10.resolve(frame.file);
3917
+ abs = path11.resolve(frame.file);
3751
3918
  } catch {
3752
3919
  continue;
3753
3920
  }
3754
- const rel = path10.relative(projectRoot, abs);
3755
- if (rel.startsWith("..") || path10.isAbsolute(rel)) continue;
3921
+ const rel = path11.relative(projectRoot, abs);
3922
+ if (rel.startsWith("..") || path11.isAbsolute(rel)) continue;
3756
3923
  if (!isNaN(frame.line)) add(abs, frame.line);
3757
3924
  }
3758
3925
  }
3759
- const absTest = path10.resolve(testFile2);
3926
+ const absTest = path11.resolve(testFile2);
3760
3927
  if (!picked.some((p) => p.absFile === absTest)) {
3761
3928
  add(absTest, extractFailingLine(errorText, testFile2, declLine));
3762
3929
  }
@@ -3764,49 +3931,23 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
3764
3931
  for (const p of picked.slice(0, maxFrames)) {
3765
3932
  const snippet = readSourceSnippet(p.absFile, p.line, context, p.line);
3766
3933
  if (snippet) {
3767
- frames.push({ file: path10.relative(projectRoot, p.absFile) || path10.basename(p.absFile), line: p.line, snippet });
3934
+ frames.push({ file: path11.relative(projectRoot, p.absFile) || path11.basename(p.absFile), line: p.line, snippet });
3768
3935
  }
3769
3936
  }
3770
3937
  return frames;
3771
3938
  }
3772
3939
  function extractFailingLine(errorText, testFile2, declarationLine) {
3773
3940
  if (!errorText) return declarationLine;
3774
- const expectedFile = path10.resolve(testFile2);
3941
+ const expectedFile = path11.resolve(testFile2);
3775
3942
  for (const frame of stackFrames(errorText)) {
3776
3943
  try {
3777
- if (path10.resolve(frame.file) === expectedFile && !isNaN(frame.line)) return frame.line;
3944
+ if (path11.resolve(frame.file) === expectedFile && !isNaN(frame.line)) return frame.line;
3778
3945
  } catch {
3779
3946
  }
3780
3947
  }
3781
3948
  return declarationLine;
3782
3949
  }
3783
3950
 
3784
- // src/internal/support/ci.ts
3785
- function detectCiIds(env) {
3786
- if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID) {
3787
- const run = env.GITHUB_RUN_ATTEMPT ? `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` : env.GITHUB_RUN_ID;
3788
- return { run, job: env.GITHUB_JOB || null };
3789
- }
3790
- if (env.GITLAB_CI && env.CI_PIPELINE_ID) return { run: env.CI_PIPELINE_ID, job: env.CI_JOB_ID || null };
3791
- if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID) return { run: env.CIRCLE_WORKFLOW_ID, job: env.CIRCLE_BUILD_NUM || null };
3792
- if (env.TRAVIS && env.TRAVIS_BUILD_ID) return { run: env.TRAVIS_BUILD_ID, job: env.TRAVIS_JOB_ID || null };
3793
- if (env.TF_BUILD && env.BUILD_BUILDID) return { run: env.BUILD_BUILDID, job: env.SYSTEM_JOBID || null };
3794
- if (env.JENKINS_URL && env.BUILD_ID) return { run: env.BUILD_ID, job: null };
3795
- if (env.BUILDKITE_BUILD_ID) return { run: env.BUILDKITE_BUILD_ID, job: env.BUILDKITE_JOB_ID || null };
3796
- if (env.TEAMCITY_BUILD_ID) return { run: env.TEAMCITY_BUILD_ID, job: null };
3797
- if (env.BITBUCKET_BUILD_NUMBER) return { run: env.BITBUCKET_BUILD_NUMBER, job: env.BITBUCKET_STEP_UUID || null };
3798
- if (env.SEMAPHORE_WORKFLOW_ID) return { run: env.SEMAPHORE_WORKFLOW_ID, job: env.SEMAPHORE_JOB_ID || null };
3799
- if (env.APPVEYOR_BUILD_ID) return { run: env.APPVEYOR_BUILD_ID, job: env.APPVEYOR_JOB_ID || null };
3800
- if (env.DRONE_BUILD_NUMBER) return { run: env.DRONE_BUILD_NUMBER, job: null };
3801
- return null;
3802
- }
3803
- function resolveRunLabel(configured, sharded) {
3804
- if (configured) return configured;
3805
- const ci = detectCiIds(process.env);
3806
- if (!ci) return null;
3807
- return !sharded && ci.job ? `${ci.run}|${ci.job}` : ci.run;
3808
- }
3809
-
3810
3951
  // src/internal/support/worker-index.ts
3811
3952
  function workerIndexOf(result) {
3812
3953
  if (!result) return null;
@@ -3892,28 +4033,28 @@ function isListMode(argv = process.argv) {
3892
4033
  }
3893
4034
 
3894
4035
  // src/public/global-setup.ts
3895
- var path14 = __toESM(require("path"));
3896
- var fs14 = __toESM(require("fs"));
4036
+ var path15 = __toESM(require("path"));
4037
+ var fs15 = __toESM(require("fs"));
3897
4038
 
3898
4039
  // src/internal/support/aria-sampling.ts
3899
- var path11 = __toESM(require("path"));
4040
+ var path12 = __toESM(require("path"));
3900
4041
  var os7 = __toESM(require("os"));
3901
- var fs11 = __toESM(require("fs"));
4042
+ var fs12 = __toESM(require("fs"));
3902
4043
  function ariaSampleIdentity(filePath2, title) {
3903
4044
  return `${filePath2}\0${title}`;
3904
4045
  }
3905
4046
  function getAriaSampleFilePath(projectName) {
3906
- return path11.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
4047
+ return path12.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
3907
4048
  }
3908
4049
  function writeAriaSampleFile(projectName, identities) {
3909
4050
  try {
3910
- fs11.writeFileSync(getAriaSampleFilePath(projectName), JSON.stringify({ projectName, identities }));
4051
+ fs12.writeFileSync(getAriaSampleFilePath(projectName), JSON.stringify({ projectName, identities }));
3911
4052
  } catch {
3912
4053
  }
3913
4054
  }
3914
4055
  function clearAriaSampleFile(projectName) {
3915
4056
  try {
3916
- fs11.rmSync(getAriaSampleFilePath(projectName), { force: true });
4057
+ fs12.rmSync(getAriaSampleFilePath(projectName), { force: true });
3917
4058
  } catch {
3918
4059
  }
3919
4060
  }
@@ -3922,7 +4063,7 @@ function loadAriaSampleSet(projectName) {
3922
4063
  if (cachedSets.has(projectName)) return cachedSets.get(projectName);
3923
4064
  let set = null;
3924
4065
  try {
3925
- const raw = fs11.readFileSync(getAriaSampleFilePath(projectName), "utf8");
4066
+ const raw = fs12.readFileSync(getAriaSampleFilePath(projectName), "utf8");
3926
4067
  const parsed2 = JSON.parse(raw);
3927
4068
  if (parsed2.projectName === projectName && Array.isArray(parsed2.identities)) {
3928
4069
  set = new Set(parsed2.identities.filter((x) => typeof x === "string"));
@@ -4052,7 +4193,7 @@ async function checkBaseUrl(target, options = {}) {
4052
4193
  } else {
4053
4194
  reason = outcome.error;
4054
4195
  }
4055
- if (attempt < attempts) await new Promise((resolve9) => setTimeout(resolve9, delayMs));
4196
+ if (attempt < attempts) await new Promise((resolve11) => setTimeout(resolve11, delayMs));
4056
4197
  }
4057
4198
  return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
4058
4199
  }
@@ -4076,14 +4217,14 @@ async function checkBaseUrls(config, options = {}) {
4076
4217
  }
4077
4218
 
4078
4219
  // src/internal/manifest/collect.ts
4079
- var fs12 = __toESM(require("fs"));
4080
- var path12 = __toESM(require("path"));
4220
+ var fs13 = __toESM(require("fs"));
4221
+ var path13 = __toESM(require("path"));
4081
4222
 
4082
4223
  // src/internal/transport/backend-request.ts
4083
4224
  var http2 = __toESM(require("http"));
4084
4225
  var https2 = __toESM(require("https"));
4085
4226
  function backendRequest(url, init) {
4086
- return new Promise((resolve9, reject) => {
4227
+ return new Promise((resolve11, reject) => {
4087
4228
  const target = new URL(url);
4088
4229
  const headers = { ...init.headers };
4089
4230
  if (init.body !== void 0) headers["Content-Length"] = Buffer.byteLength(init.body);
@@ -4098,7 +4239,7 @@ function backendRequest(url, init) {
4098
4239
  });
4099
4240
  res.on(
4100
4241
  "end",
4101
- () => resolve9({ status: res.statusCode ?? 0, headers: res.headers, text: Buffer.concat(chunks).toString("utf8") })
4242
+ () => resolve11({ status: res.statusCode ?? 0, headers: res.headers, text: Buffer.concat(chunks).toString("utf8") })
4102
4243
  );
4103
4244
  res.on("error", reject);
4104
4245
  res.on("close", () => {
@@ -4143,10 +4284,10 @@ function hasInstrumentationHeader(headers) {
4143
4284
  return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
4144
4285
  }
4145
4286
  function readCommittedManifest(configDir) {
4146
- const file = path12.join(configDir, COMMITTED_MANIFEST_FILE);
4287
+ const file = path13.join(configDir, COMMITTED_MANIFEST_FILE);
4147
4288
  try {
4148
- if (!fs12.existsSync(file)) return null;
4149
- return parseManifestJson(fs12.readFileSync(file, "utf8"));
4289
+ if (!fs13.existsSync(file)) return null;
4290
+ return parseManifestJson(fs13.readFileSync(file, "utf8"));
4150
4291
  } catch {
4151
4292
  return null;
4152
4293
  }
@@ -4172,8 +4313,8 @@ function backendManifestFetch(insecure) {
4172
4313
  function instrumentationManifestUrls(baseUrl) {
4173
4314
  const base = new URL(baseUrl);
4174
4315
  const root = new URL(INSTRUMENTATION_MANIFEST_PATH, base).toString();
4175
- const path32 = base.pathname.replace(/\/+$/, "");
4176
- return path32 ? [new URL(`${path32}${INSTRUMENTATION_MANIFEST_PATH}`, base).toString(), root] : [root];
4316
+ const path34 = base.pathname.replace(/\/+$/, "");
4317
+ return path34 ? [new URL(`${path34}${INSTRUMENTATION_MANIFEST_PATH}`, base).toString(), root] : [root];
4177
4318
  }
4178
4319
  async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
4179
4320
  try {
@@ -4311,13 +4452,13 @@ function normalizeFunctionRanges(ranges) {
4311
4452
  }
4312
4453
  function normalizeCoveragePath(raw) {
4313
4454
  if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
4314
- const path32 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4315
- if (!path32 || path32.startsWith("../") || path32.startsWith("/") || /^[A-Za-z]:\//.test(path32)) return null;
4316
- if (/(^|\/)node_modules\//.test(path32)) return null;
4317
- return path32;
4455
+ const path34 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4456
+ if (!path34 || path34.startsWith("../") || path34.startsWith("/") || /^[A-Za-z]:\//.test(path34)) return null;
4457
+ if (/(^|\/)node_modules\//.test(path34)) return null;
4458
+ return path34;
4318
4459
  }
4319
- function fileEntry(path32, runtime, blob, functions = [], lines = []) {
4320
- const file = { path: path32, runtime };
4460
+ function fileEntry(path34, runtime, blob, functions = [], lines = []) {
4461
+ const file = { path: path34, runtime };
4321
4462
  if (blob && (functions.length > 0 || lines.length > 0)) file.blob = blob;
4322
4463
  if (functions.length > 0) file.functions = functions;
4323
4464
  if (lines.length > 0) file.lines = lines;
@@ -4336,11 +4477,11 @@ function sanitizeCoverage(raw) {
4336
4477
  const files = [];
4337
4478
  for (const item of raw.files) {
4338
4479
  if (!isRecord(item) || !isRuntime(item.runtime)) continue;
4339
- const path32 = normalizeCoveragePath(item.path);
4340
- if (path32 === null) continue;
4480
+ const path34 = normalizeCoveragePath(item.path);
4481
+ if (path34 === null) continue;
4341
4482
  const blob = typeof item.blob === "string" && BLOB_ID.test(item.blob) ? item.blob : void 0;
4342
4483
  files.push(
4343
- fileEntry(path32, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
4484
+ fileEntry(path34, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
4344
4485
  );
4345
4486
  }
4346
4487
  const merged = mergeCoverage(captureOf(files, raw.incomplete === true));
@@ -4405,8 +4546,8 @@ function reduceCoverage(capture, granularity) {
4405
4546
 
4406
4547
  // src/internal/collect/source-checksums.ts
4407
4548
  var crypto4 = __toESM(require("crypto"));
4408
- var fs13 = __toESM(require("fs"));
4409
- var path13 = __toESM(require("path"));
4549
+ var fs14 = __toESM(require("fs"));
4550
+ var path14 = __toESM(require("path"));
4410
4551
  var CHECKSUM = /^(sha1|sha256):[0-9a-f]{40,64}$/;
4411
4552
  function checksumsOf(raw) {
4412
4553
  if (!Array.isArray(raw)) return [];
@@ -4471,10 +4612,10 @@ var SourceChecksums = class {
4471
4612
  }
4472
4613
  async read(relativePath2) {
4473
4614
  try {
4474
- const root = path13.resolve(this.repoRoot());
4475
- const file = path13.resolve(root, relativePath2);
4476
- if (!file.startsWith(root + path13.sep)) return null;
4477
- return contentChecksums(await fs13.promises.readFile(file));
4615
+ const root = path14.resolve(this.repoRoot());
4616
+ const file = path14.resolve(root, relativePath2);
4617
+ if (!file.startsWith(root + path14.sep)) return null;
4618
+ return contentChecksums(await fs14.promises.readFile(file));
4478
4619
  } catch {
4479
4620
  return null;
4480
4621
  }
@@ -4619,13 +4760,13 @@ var ServerCoverageCollector = class {
4619
4760
  if (parts.length === 0 || this.closer.signal.aborted) {
4620
4761
  return Promise.resolve({ files: [], incomplete: truncated || parts.length > 0 });
4621
4762
  }
4622
- return new Promise((resolve9) => {
4763
+ return new Promise((resolve11) => {
4623
4764
  const entry = {
4624
4765
  pending: parts.length,
4625
4766
  captures: [],
4626
4767
  checksums: /* @__PURE__ */ new Map(),
4627
4768
  incomplete: truncated,
4628
- resolve: resolve9
4769
+ resolve: resolve11
4629
4770
  };
4630
4771
  for (const [origin, ids] of parts) this.enqueue(origin, { entry, ids });
4631
4772
  });
@@ -4951,14 +5092,14 @@ var ServerCoverageCollector = class {
4951
5092
  // src/public/global-setup.ts
4952
5093
  function createGlobalSetup(options, userSetup) {
4953
5094
  return async function globalSetupFn(config) {
4954
- const piwiReporterPath = path14.resolve(__dirname, "./index.js");
5095
+ const piwiReporterPath = path15.resolve(__dirname, "./index.js");
4955
5096
  let inlineReporterOptions = {};
4956
5097
  if (Array.isArray(config?.reporter)) {
4957
5098
  for (const r of config.reporter) {
4958
5099
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
4959
5100
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
4960
5101
  try {
4961
- return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
5102
+ return path15.resolve(require.resolve(r[0])) === piwiReporterPath;
4962
5103
  } catch {
4963
5104
  return false;
4964
5105
  }
@@ -4969,7 +5110,10 @@ function createGlobalSetup(options, userSetup) {
4969
5110
  }
4970
5111
  }
4971
5112
  }
4972
- const opts = resolveOptions({ ...inlineReporterOptions, ...options });
5113
+ const opts = resolveOptions(
5114
+ { ...inlineReporterOptions, ...options },
5115
+ configDirFromConfig(config)
5116
+ );
4973
5117
  const logger = new Logger(opts.verbose ?? false);
4974
5118
  if (isUiMode()) {
4975
5119
  logger.debug("UI mode detected \u2014 skipping run registration.");
@@ -4994,7 +5138,7 @@ function createGlobalSetup(options, userSetup) {
4994
5138
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
4995
5139
  if (r[0].toLowerCase().includes("piwi")) return true;
4996
5140
  try {
4997
- return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
5141
+ return path15.resolve(require.resolve(r[0])) === piwiReporterPath;
4998
5142
  } catch {
4999
5143
  return false;
5000
5144
  }
@@ -5024,7 +5168,7 @@ function createGlobalSetup(options, userSetup) {
5024
5168
  auth
5025
5169
  );
5026
5170
  if (response?.runId && response?.setupToken) {
5027
- fs14.writeFileSync(
5171
+ fs15.writeFileSync(
5028
5172
  getSetupFilePath(opts.projectName),
5029
5173
  JSON.stringify({
5030
5174
  runId: response.runId,
@@ -5078,11 +5222,11 @@ function createGlobalSetup(options, userSetup) {
5078
5222
  }
5079
5223
 
5080
5224
  // src/public/config-wrapper.ts
5081
- var path15 = __toESM(require("path"));
5082
- var fs17 = __toESM(require("fs"));
5225
+ var path16 = __toESM(require("path"));
5226
+ var fs18 = __toESM(require("fs"));
5083
5227
 
5084
5228
  // src/internal/probe/mode.ts
5085
- var fs15 = __toESM(require("fs"));
5229
+ var fs16 = __toESM(require("fs"));
5086
5230
 
5087
5231
  // src/internal/probe/plan.ts
5088
5232
  function probeRunMetadata(base = {}) {
@@ -5094,8 +5238,8 @@ function parseRouteKey(routeKey2) {
5094
5238
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
5095
5239
  }
5096
5240
  function patternToRegex(pattern) {
5097
- const path32 = pattern.split("?")[0] ?? pattern;
5098
- const escaped = path32.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
5241
+ const path34 = pattern.split("?")[0] ?? pattern;
5242
+ const escaped = path34.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
5099
5243
  return new RegExp(`^${escaped}$`);
5100
5244
  }
5101
5245
  function suitePathEqual(a, b) {
@@ -5118,13 +5262,13 @@ function matchProbeItem(plan, test) {
5118
5262
  function requestMatchesRoute(method, url, routeKey2) {
5119
5263
  const { method: rm, pattern } = parseRouteKey(routeKey2);
5120
5264
  if (rm && rm !== method.toUpperCase()) return false;
5121
- let path32;
5265
+ let path34;
5122
5266
  try {
5123
- path32 = new URL(url).pathname;
5267
+ path34 = new URL(url).pathname;
5124
5268
  } catch {
5125
- path32 = url.split("?")[0] ?? url;
5269
+ path34 = url.split("?")[0] ?? url;
5126
5270
  }
5127
- return patternToRegex(pattern).test(path32);
5271
+ return patternToRegex(pattern).test(path34);
5128
5272
  }
5129
5273
  function createProbeState() {
5130
5274
  return { navigated: false, matches: 0 };
@@ -5146,10 +5290,10 @@ function isProbeMode() {
5146
5290
  function loadPlan() {
5147
5291
  if (planCache !== void 0) return planCache;
5148
5292
  planCache = null;
5149
- const path32 = process.env[PIWI_PROBE_ENV.plan];
5150
- if (path32) {
5293
+ const path34 = process.env[PIWI_PROBE_ENV.plan];
5294
+ if (path34) {
5151
5295
  try {
5152
- planCache = JSON.parse(fs15.readFileSync(path32, "utf8"));
5296
+ planCache = JSON.parse(fs16.readFileSync(path34, "utf8"));
5153
5297
  } catch {
5154
5298
  planCache = null;
5155
5299
  }
@@ -5173,17 +5317,17 @@ function outcomeFromStatus(status, applied) {
5173
5317
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
5174
5318
  }
5175
5319
  function recordProbeOutcome(line) {
5176
- const path32 = process.env[PIWI_PROBE_ENV.results];
5177
- if (!path32) return;
5320
+ const path34 = process.env[PIWI_PROBE_ENV.results];
5321
+ if (!path34) return;
5178
5322
  try {
5179
- fs15.appendFileSync(path32, `${JSON.stringify(line)}
5323
+ fs16.appendFileSync(path34, `${JSON.stringify(line)}
5180
5324
  `);
5181
5325
  } catch {
5182
5326
  }
5183
5327
  }
5184
5328
 
5185
5329
  // src/internal/flake/mode.ts
5186
- var fs16 = __toESM(require("fs"));
5330
+ var fs17 = __toESM(require("fs"));
5187
5331
 
5188
5332
  // ../core/src/error-signature.ts
5189
5333
  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;
@@ -5559,7 +5703,7 @@ function originOf2(url) {
5559
5703
  return null;
5560
5704
  }
5561
5705
  }
5562
- var sleep = (ms) => new Promise((resolve9) => setTimeout(resolve9, Math.max(0, ms)));
5706
+ var sleep = (ms) => new Promise((resolve11) => setTimeout(resolve11, Math.max(0, ms)));
5563
5707
  async function performRouteAction(route, action, startedAt) {
5564
5708
  switch (action.kind) {
5565
5709
  case "abort":
@@ -5683,7 +5827,7 @@ function loadFlakePlan() {
5683
5827
  if (planCache2 && planCache2.file === file) return planCache2.plan;
5684
5828
  let text;
5685
5829
  try {
5686
- text = fs16.readFileSync(file, "utf8");
5830
+ text = fs17.readFileSync(file, "utf8");
5687
5831
  } catch (error) {
5688
5832
  throw new Error(`[Piwi Dashboard] Cannot read the flake plan ${file}: ${errorMessage(error)}`);
5689
5833
  }
@@ -5844,7 +5988,7 @@ function recordFlakeResult(line) {
5844
5988
  const file = process.env[PIWI_FLAKE_ENV.results]?.trim();
5845
5989
  if (!file) return;
5846
5990
  try {
5847
- fs16.appendFileSync(file, `${JSON.stringify(line)}
5991
+ fs17.appendFileSync(file, `${JSON.stringify(line)}
5848
5992
  `);
5849
5993
  } catch {
5850
5994
  }
@@ -5915,11 +6059,11 @@ function injectReporter(reporter, piwiOptions) {
5915
6059
  }
5916
6060
  function resolveSetupModule() {
5917
6061
  const candidates = [
5918
- path15.join(__dirname, "global-setup-module.js"),
5919
- path15.join(__dirname, "..", "global-setup-module.js"),
5920
- path15.join(__dirname, "..", "global-setup-module.ts")
6062
+ path16.join(__dirname, "global-setup-module.js"),
6063
+ path16.join(__dirname, "..", "global-setup-module.js"),
6064
+ path16.join(__dirname, "..", "global-setup-module.ts")
5921
6065
  ];
5922
- return candidates.find((candidate) => fs17.existsSync(candidate)) ?? candidates[0];
6066
+ return candidates.find((candidate) => fs18.existsSync(candidate)) ?? candidates[0];
5923
6067
  }
5924
6068
  function wrapConfig(config, piwiOptions) {
5925
6069
  if (piwiOptions) applyOptionsToEnv(piwiOptions);
@@ -6315,8 +6459,8 @@ function buildErrorText(result, base = process.cwd()) {
6315
6459
  var import_node_crypto2 = require("crypto");
6316
6460
 
6317
6461
  // src/internal/support/ci-output.ts
6318
- var fs18 = __toESM(require("fs"));
6319
- var path16 = __toESM(require("path"));
6462
+ var fs19 = __toESM(require("fs"));
6463
+ var path17 = __toESM(require("path"));
6320
6464
  var SUMMARY_MAX_FAILURES = 20;
6321
6465
  function emitRunOutputs(output, logger, outputFile, env = process.env) {
6322
6466
  logger.info(`View run: ${output.runUrl}`);
@@ -6326,9 +6470,9 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
6326
6470
  }
6327
6471
  function writeOutputFile(file, output, logger) {
6328
6472
  try {
6329
- const dir = path16.dirname(file);
6330
- if (dir && dir !== ".") fs18.mkdirSync(dir, { recursive: true });
6331
- fs18.writeFileSync(
6473
+ const dir = path17.dirname(file);
6474
+ if (dir && dir !== ".") fs19.mkdirSync(dir, { recursive: true });
6475
+ fs19.writeFileSync(
6332
6476
  file,
6333
6477
  JSON.stringify(
6334
6478
  {
@@ -6408,7 +6552,7 @@ function emitGitLabDotenv(output, env, logger) {
6408
6552
  if (output.projectId != null) lines.push(`PIWI_PROJECT_ID=${output.projectId}`);
6409
6553
  if (output.ciBuildUrl) lines.push(`PIWI_CI_BUILD_URL=${output.ciBuildUrl}`);
6410
6554
  try {
6411
- fs18.writeFileSync(file, lines.join("\n") + "\n");
6555
+ fs19.writeFileSync(file, lines.join("\n") + "\n");
6412
6556
  logger.info(`Wrote GitLab dotenv report to ${file} (declare it as artifacts:reports:dotenv)`);
6413
6557
  } catch (error) {
6414
6558
  logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
@@ -6425,7 +6569,7 @@ function emitResourceSummary(lines, logger, env = process.env) {
6425
6569
  }
6426
6570
  function appendFileLines(file, lines, logger, label) {
6427
6571
  try {
6428
- fs18.appendFileSync(file, lines.join("\n") + "\n");
6572
+ fs19.appendFileSync(file, lines.join("\n") + "\n");
6429
6573
  } catch (error) {
6430
6574
  logger.warn(`Failed to write GitHub Actions ${label}: ${errorMessage(error)}`);
6431
6575
  }
@@ -6438,8 +6582,8 @@ function ciBuildUrlFromMetadata(metadata) {
6438
6582
 
6439
6583
  // src/internal/submit/run-submitter.ts
6440
6584
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
6441
- function markSubmitFallback(run, path32, reason) {
6442
- run.metadata.ingestHealth = { submitFallback: { path: path32, reason } };
6585
+ function markSubmitFallback(run, path34, reason) {
6586
+ run.metadata.ingestHealth = { submitFallback: { path: path34, reason } };
6443
6587
  }
6444
6588
  var RunSubmitter = class {
6445
6589
  /**
@@ -6700,7 +6844,7 @@ var RunSubmitter = class {
6700
6844
  */
6701
6845
  stopForMissingAuth(run, payload) {
6702
6846
  this.logger.error(
6703
- `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.`
6847
+ `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 PIWI_API_KEY in the environment or in the .env file next to the Playwright config.`
6704
6848
  );
6705
6849
  this.logRecoverySaved();
6706
6850
  this.saveRecovery(payload);
@@ -6715,13 +6859,13 @@ var RunSubmitter = class {
6715
6859
  };
6716
6860
 
6717
6861
  // src/internal/capture/resource-ledger.ts
6718
- var fs20 = __toESM(require("fs"));
6719
- var path19 = __toESM(require("path"));
6862
+ var fs21 = __toESM(require("fs"));
6863
+ var path20 = __toESM(require("path"));
6720
6864
  var import_node_module2 = require("module");
6721
6865
 
6722
6866
  // src/internal/capture/quiet-capture.ts
6723
6867
  var import_node_module = require("module");
6724
- var path17 = __toESM(require("path"));
6868
+ var path18 = __toESM(require("path"));
6725
6869
  var BOXED_PREFIX_EXPORTS = [
6726
6870
  [
6727
6871
  "playwright-core/lib/coreBundle",
@@ -6758,7 +6902,7 @@ function resolveBoxing() {
6758
6902
  if (typeof set !== "function") continue;
6759
6903
  return {
6760
6904
  set,
6761
- prefixes: [path17.dirname(playwrightPackageJson), path17.join(root, "dist") + path17.sep]
6905
+ prefixes: [path18.dirname(playwrightPackageJson), path18.join(root, "dist") + path18.sep]
6762
6906
  };
6763
6907
  }
6764
6908
  } catch {
@@ -6790,12 +6934,12 @@ function internalCall(owner, run) {
6790
6934
  var import_node_perf_hooks = require("perf_hooks");
6791
6935
 
6792
6936
  // src/internal/support/system-readers.ts
6793
- var fs19 = __toESM(require("fs"));
6794
- var path18 = __toESM(require("path"));
6937
+ var fs20 = __toESM(require("fs"));
6938
+ var path19 = __toESM(require("path"));
6795
6939
  var CLK_TCK = 100;
6796
6940
  function readText(file) {
6797
6941
  try {
6798
- return fs19.readFileSync(file, "utf8");
6942
+ return fs20.readFileSync(file, "utf8");
6799
6943
  } catch {
6800
6944
  return null;
6801
6945
  }
@@ -6929,45 +7073,45 @@ var ProcFs = class {
6929
7073
  }
6930
7074
  /** Whether this looks like a Linux `/proc` at all. */
6931
7075
  available() {
6932
- return fs19.existsSync(path18.join(this.root, "stat"));
7076
+ return fs20.existsSync(path19.join(this.root, "stat"));
6933
7077
  }
6934
7078
  /** Every process, keyed by pid. */
6935
7079
  table() {
6936
7080
  const out = /* @__PURE__ */ new Map();
6937
7081
  let names;
6938
7082
  try {
6939
- names = fs19.readdirSync(this.root);
7083
+ names = fs20.readdirSync(this.root);
6940
7084
  } catch {
6941
7085
  return out;
6942
7086
  }
6943
7087
  for (const name of names) {
6944
7088
  if (!/^\d+$/.test(name)) continue;
6945
- const text = readText(path18.join(this.root, name, "stat"));
7089
+ const text = readText(path19.join(this.root, name, "stat"));
6946
7090
  const stat = text ? parseProcStat(Number(name), text) : null;
6947
7091
  if (stat) out.set(stat.pid, stat);
6948
7092
  }
6949
7093
  return out;
6950
7094
  }
6951
7095
  stat(pid) {
6952
- const text = readText(path18.join(this.root, String(pid), "stat"));
7096
+ const text = readText(path19.join(this.root, String(pid), "stat"));
6953
7097
  return text ? parseProcStat(pid, text) : null;
6954
7098
  }
6955
7099
  runWaitMs(pid) {
6956
- const text = readText(path18.join(this.root, String(pid), "schedstat"));
7100
+ const text = readText(path19.join(this.root, String(pid), "schedstat"));
6957
7101
  return text ? parseSchedstat(text) : null;
6958
7102
  }
6959
7103
  cmdline(pid) {
6960
- return (readText(path18.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
7104
+ return (readText(path19.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
6961
7105
  }
6962
7106
  /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
6963
7107
  statusKb(pid, key) {
6964
- const text = readText(path18.join(this.root, String(pid), "status"));
7108
+ const text = readText(path19.join(this.root, String(pid), "status"));
6965
7109
  return text ? parseKbField(text, key) : null;
6966
7110
  }
6967
7111
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
6968
7112
  async pssKb(pid) {
6969
7113
  try {
6970
- return parseKbField(await fs19.promises.readFile(path18.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
7114
+ return parseKbField(await fs20.promises.readFile(path19.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
6971
7115
  } catch {
6972
7116
  return null;
6973
7117
  }
@@ -6975,29 +7119,29 @@ var ProcFs = class {
6975
7119
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
6976
7120
  resetPeakRss(pid) {
6977
7121
  try {
6978
- fs19.writeFileSync(path18.join(this.root, String(pid), "clear_refs"), "5");
7122
+ fs20.writeFileSync(path19.join(this.root, String(pid), "clear_refs"), "5");
6979
7123
  return true;
6980
7124
  } catch {
6981
7125
  return false;
6982
7126
  }
6983
7127
  }
6984
7128
  cpuTicks() {
6985
- const text = readText(path18.join(this.root, "stat"));
7129
+ const text = readText(path19.join(this.root, "stat"));
6986
7130
  return text ? parseCpuTicks(text) : null;
6987
7131
  }
6988
7132
  /** Total stall time of a resource's pressure file, in µs. */
6989
7133
  pressureUs(resource) {
6990
- const text = readText(path18.join(this.root, "pressure", resource));
7134
+ const text = readText(path19.join(this.root, "pressure", resource));
6991
7135
  return text ? parsePsiTotal(text) : null;
6992
7136
  }
6993
7137
  memAvailableKb() {
6994
- const text = readText(path18.join(this.root, "meminfo"));
7138
+ const text = readText(path19.join(this.root, "meminfo"));
6995
7139
  return text ? parseKbField(text, "MemAvailable") : null;
6996
7140
  }
6997
7141
  /** Open file descriptors of a process. */
6998
7142
  fdCount(pid) {
6999
7143
  try {
7000
- return fs19.readdirSync(path18.join(this.root, String(pid), "fd")).length;
7144
+ return fs20.readdirSync(path19.join(this.root, String(pid), "fd")).length;
7001
7145
  } catch {
7002
7146
  return null;
7003
7147
  }
@@ -7018,7 +7162,7 @@ var CgroupFs = class {
7018
7162
  }
7019
7163
  /** The directory of a controller (v1) or of the unified hierarchy (v2), when it exists. */
7020
7164
  dirs() {
7021
- const text = readText(path18.join(this.procRoot, String(this.pid), "cgroup"));
7165
+ const text = readText(path19.join(this.procRoot, String(this.pid), "cgroup"));
7022
7166
  if (!text) return null;
7023
7167
  const v1 = /* @__PURE__ */ new Map();
7024
7168
  let v2 = null;
@@ -7029,21 +7173,21 @@ var CgroupFs = class {
7029
7173
  else for (const controller of match[1].split(",")) v1.set(controller, match[2]);
7030
7174
  }
7031
7175
  const pick = (mount, sub) => {
7032
- const nested = path18.join(mount, sub);
7033
- if (fs19.existsSync(nested)) return nested;
7034
- return fs19.existsSync(mount) ? mount : null;
7176
+ const nested = path19.join(mount, sub);
7177
+ if (fs20.existsSync(nested)) return nested;
7178
+ return fs20.existsSync(mount) ? mount : null;
7035
7179
  };
7036
7180
  if (v1.has("memory") || v1.has("cpu")) {
7037
7181
  const memoryPath = v1.get("memory");
7038
7182
  const cpuPath = v1.get("cpu");
7039
- const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path18.join(this.cgroupRoot, name)).find((dir) => fs19.existsSync(dir));
7183
+ const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path19.join(this.cgroupRoot, name)).find((dir) => fs20.existsSync(dir));
7040
7184
  return {
7041
7185
  version: 1,
7042
- memory: memoryPath !== void 0 ? pick(path18.join(this.cgroupRoot, "memory"), memoryPath) : null,
7186
+ memory: memoryPath !== void 0 ? pick(path19.join(this.cgroupRoot, "memory"), memoryPath) : null,
7043
7187
  cpu: cpuPath !== void 0 && cpuMount ? pick(cpuMount, cpuPath) : null
7044
7188
  };
7045
7189
  }
7046
- if (v2 !== null && fs19.existsSync(path18.join(this.cgroupRoot, "cgroup.controllers"))) {
7190
+ if (v2 !== null && fs20.existsSync(path19.join(this.cgroupRoot, "cgroup.controllers"))) {
7047
7191
  const dir = pick(this.cgroupRoot, v2);
7048
7192
  return { version: 2, memory: dir, cpu: dir };
7049
7193
  }
@@ -7052,7 +7196,7 @@ var CgroupFs = class {
7052
7196
  read() {
7053
7197
  const dirs = this.dirs();
7054
7198
  if (!dirs) return null;
7055
- const file = (dir, name) => dir ? readText(path18.join(dir, name)) : null;
7199
+ const file = (dir, name) => dir ? readText(path19.join(dir, name)) : null;
7056
7200
  if (dirs.version === 2) {
7057
7201
  const max = file(dirs.memory, "memory.max");
7058
7202
  const cpuMax = file(dirs.cpu, "cpu.max")?.trim().split(/\s+/);
@@ -7238,10 +7382,10 @@ var PAGE_READ_TIMEOUT_MS = 1e3;
7238
7382
  var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
7239
7383
  var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
7240
7384
  function relativePath(file) {
7241
- return path19.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path19.sep).join("/");
7385
+ return path20.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path20.sep).join("/");
7242
7386
  }
7243
7387
  function isLibraryFile(file) {
7244
- return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path19.sep}node_modules${path19.sep}`) || file.includes("/node_modules/") || file === __filename;
7388
+ return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path20.sep}node_modules${path20.sep}`) || file.includes("/node_modules/") || file === __filename;
7245
7389
  }
7246
7390
  function userSite(stack) {
7247
7391
  for (const line of (stack ?? "").split("\n").slice(1)) {
@@ -7289,7 +7433,7 @@ function playwrightTestInfoSource() {
7289
7433
  try {
7290
7434
  const packageJson = resolvePlaywrightPackageJson();
7291
7435
  if (!packageJson) return () => null;
7292
- const globals = (0, import_node_module2.createRequire)(packageJson)(path19.join(path19.dirname(packageJson), "lib", "globals.js"));
7436
+ const globals = (0, import_node_module2.createRequire)(packageJson)(path20.join(path20.dirname(packageJson), "lib", "globals.js"));
7293
7437
  const read = globals.currentTestInfo;
7294
7438
  if (typeof read !== "function") return () => null;
7295
7439
  return () => {
@@ -7318,15 +7462,15 @@ function countHandles() {
7318
7462
  async function settledHandles(start) {
7319
7463
  const end = countHandles();
7320
7464
  if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
7321
- await new Promise((resolve9) => setTimeout(resolve9, 0));
7465
+ await new Promise((resolve11) => setTimeout(resolve11, 0));
7322
7466
  return countHandles();
7323
7467
  }
7324
7468
  function settleWithin(promise, ms) {
7325
7469
  let timer;
7326
7470
  return Promise.race([
7327
7471
  promise.catch(() => void 0),
7328
- new Promise((resolve9) => {
7329
- timer = setTimeout(resolve9, ms);
7472
+ new Promise((resolve11) => {
7473
+ timer = setTimeout(resolve11, ms);
7330
7474
  })
7331
7475
  ]).finally(() => clearTimeout(timer));
7332
7476
  }
@@ -7455,7 +7599,7 @@ var ResourceLedger = class {
7455
7599
  const census = this.census(null);
7456
7600
  if (this.resultsFile) {
7457
7601
  try {
7458
- fs20.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
7602
+ fs21.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
7459
7603
  `);
7460
7604
  } catch {
7461
7605
  }
@@ -7778,7 +7922,7 @@ async function recordResourceCensus(testInfo) {
7778
7922
  }
7779
7923
 
7780
7924
  // src/internal/collect/resource-verdicts.ts
7781
- var path20 = __toESM(require("path"));
7925
+ var path21 = __toESM(require("path"));
7782
7926
  var OPEN_STEP_TITLES = {
7783
7927
  "Create context": "context",
7784
7928
  "Launch persistent context": "context",
@@ -7810,7 +7954,7 @@ function userFixturesOf(steps) {
7810
7954
  const walk = (list) => {
7811
7955
  for (const step of list) {
7812
7956
  const file = step.location?.file;
7813
- if (step.category === "fixture" && file && !file.includes(`${path20.sep}node_modules${path20.sep}`)) {
7957
+ if (step.category === "fixture" && file && !file.includes(`${path21.sep}node_modules${path21.sep}`)) {
7814
7958
  const name = step.title.replace(/^(?:Fixture\s+|fixture:\s*)/, "").replace(/^"(.*)"$/, "$1");
7815
7959
  if (!name.startsWith("piwi")) out.push(`${name} (${relativeFile(file)}:${step.location.line})`);
7816
7960
  }
@@ -7843,7 +7987,7 @@ function tallyLifecycleSteps(file, steps) {
7843
7987
  return tally;
7844
7988
  }
7845
7989
  function relativeFile(file) {
7846
- return path20.relative(process.cwd(), file).split(path20.sep).join("/");
7990
+ return path21.relative(process.cwd(), file).split(path21.sep).join("/");
7847
7991
  }
7848
7992
  function buildResourceReport(input) {
7849
7993
  const findings = [];
@@ -8201,9 +8345,9 @@ function formatResourceSummary(report, leakCheck, maxLines = 10) {
8201
8345
  }
8202
8346
 
8203
8347
  // src/internal/collect/process-sampler.ts
8204
- var fs21 = __toESM(require("fs"));
8348
+ var fs22 = __toESM(require("fs"));
8205
8349
  var os8 = __toESM(require("os"));
8206
- var path21 = __toESM(require("path"));
8350
+ var path22 = __toESM(require("path"));
8207
8351
  var import_node_child_process2 = require("child_process");
8208
8352
  var WALK_FILE_CAP = 1e4;
8209
8353
  var SERIES_CAP = 3600;
@@ -8243,17 +8387,17 @@ function nodeCpuTicks() {
8243
8387
  return { busy, idle, iowait: 0, steal: 0, total: busy + idle };
8244
8388
  }
8245
8389
  function defaultPs() {
8246
- return new Promise((resolve9, reject) => {
8390
+ return new Promise((resolve11, reject) => {
8247
8391
  (0, import_node_child_process2.execFile)(
8248
8392
  "ps",
8249
8393
  ["-A", "-o", "pid=,ppid=,rss=,time=,command="],
8250
8394
  { timeout: 2e3, maxBuffer: 8 * 1024 * 1024 },
8251
- (error, stdout) => error ? reject(error) : resolve9(stdout)
8395
+ (error, stdout) => error ? reject(error) : resolve11(stdout)
8252
8396
  );
8253
8397
  });
8254
8398
  }
8255
8399
  function defaultWindowsQuery() {
8256
- return new Promise((resolve9, reject) => {
8400
+ return new Promise((resolve11, reject) => {
8257
8401
  (0, import_node_child_process2.execFile)(
8258
8402
  "powershell.exe",
8259
8403
  [
@@ -8263,18 +8407,18 @@ function defaultWindowsQuery() {
8263
8407
  "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $PID -and $_.ParentProcessId -ne $PID } | Select-Object ProcessId,ParentProcessId,WorkingSetSize,UserModeTime,KernelModeTime,Name,CommandLine | ConvertTo-Json -Compress"
8264
8408
  ],
8265
8409
  { timeout: 1e4, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
8266
- (error, stdout) => error ? reject(error) : resolve9(stdout)
8410
+ (error, stdout) => error ? reject(error) : resolve11(stdout)
8267
8411
  );
8268
8412
  });
8269
8413
  }
8270
8414
  function freeBytes(dir) {
8271
- let current = path21.resolve(dir);
8415
+ let current = path22.resolve(dir);
8272
8416
  for (; ; ) {
8273
8417
  try {
8274
- const stats = fs21.statfsSync(current);
8418
+ const stats = fs22.statfsSync(current);
8275
8419
  return stats.bavail * stats.bsize;
8276
8420
  } catch {
8277
- const parent = path21.dirname(current);
8421
+ const parent = path22.dirname(current);
8278
8422
  if (parent === current) return null;
8279
8423
  current = parent;
8280
8424
  }
@@ -8284,18 +8428,18 @@ async function dirBytes(dir, budget) {
8284
8428
  let bytes = 0;
8285
8429
  let entries;
8286
8430
  try {
8287
- entries = await fs21.promises.readdir(dir, { withFileTypes: true });
8431
+ entries = await fs22.promises.readdir(dir, { withFileTypes: true });
8288
8432
  } catch {
8289
8433
  return 0;
8290
8434
  }
8291
8435
  for (const entry of entries) {
8292
8436
  if (budget.files <= 0) return bytes;
8293
- const full = path21.join(dir, entry.name);
8437
+ const full = path22.join(dir, entry.name);
8294
8438
  if (entry.isDirectory()) bytes += await dirBytes(full, budget);
8295
8439
  else if (entry.isFile()) {
8296
8440
  budget.files--;
8297
8441
  try {
8298
- bytes += (await fs21.promises.lstat(full)).size;
8442
+ bytes += (await fs22.promises.lstat(full)).size;
8299
8443
  } catch {
8300
8444
  }
8301
8445
  }
@@ -8335,7 +8479,7 @@ var RunSampler = class {
8335
8479
  this.platform = options.platform ?? process.platform;
8336
8480
  this.proc = new ProcFs(options.procRoot);
8337
8481
  this.cgroup = new CgroupFs(options.procRoot, options.cgroupRoot);
8338
- this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path21.resolve(dir)))];
8482
+ this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path22.resolve(dir)))];
8339
8483
  this.tmpDir = options.tmpDir ?? os8.tmpdir();
8340
8484
  this.intervalMs = options.intervalMs ?? 1e3;
8341
8485
  this.heavyEvery = Math.max(1, options.heavyEvery ?? 5);
@@ -8537,7 +8681,7 @@ var RunSampler = class {
8537
8681
  } catch {
8538
8682
  return;
8539
8683
  }
8540
- this.noteTree(list, (proc) => roleOf(path21.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
8684
+ this.noteTree(list, (proc) => roleOf(path22.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
8541
8685
  }
8542
8686
  async sampleWindowsTree(at) {
8543
8687
  let list;
@@ -8593,7 +8737,7 @@ var RunSampler = class {
8593
8737
  let bytes = 0;
8594
8738
  for (const dir of this.outputDirs) bytes += await dirBytes(dir, budget);
8595
8739
  for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs >= this.startedAt - 1e3)) {
8596
- bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
8740
+ bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
8597
8741
  }
8598
8742
  if (budget.files <= 0) this.peakInUseIsLowerBound = true;
8599
8743
  this.peakInUse = Math.max(this.peakInUse ?? 0, bytes);
@@ -8602,7 +8746,7 @@ var RunSampler = class {
8602
8746
  async playwrightTempEntries(keep) {
8603
8747
  let names;
8604
8748
  try {
8605
- names = await fs21.promises.readdir(this.tmpDir);
8749
+ names = await fs22.promises.readdir(this.tmpDir);
8606
8750
  } catch {
8607
8751
  return [];
8608
8752
  }
@@ -8610,7 +8754,7 @@ var RunSampler = class {
8610
8754
  for (const name of names) {
8611
8755
  if (!PLAYWRIGHT_TEMP.test(name)) continue;
8612
8756
  try {
8613
- if (keep((await fs21.promises.stat(path21.join(this.tmpDir, name))).mtimeMs)) out.push(name);
8757
+ if (keep((await fs22.promises.stat(path22.join(this.tmpDir, name))).mtimeMs)) out.push(name);
8614
8758
  } catch {
8615
8759
  }
8616
8760
  }
@@ -8621,7 +8765,7 @@ var RunSampler = class {
8621
8765
  const budget = { files: WALK_FILE_CAP };
8622
8766
  let bytes = 0;
8623
8767
  for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs < this.startedAt - 1e3)) {
8624
- bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
8768
+ bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
8625
8769
  }
8626
8770
  return bytes;
8627
8771
  } catch {
@@ -9541,6 +9685,7 @@ function caseLocateUrl(serverUrl, runId, test) {
9541
9685
  `retry=${test.retry}`
9542
9686
  ];
9543
9687
  if (test.browser) params.push(`browser=${encodeURIComponent(test.browser)}`);
9688
+ if (test.repeatEachIndex) params.push(`repeat=${test.repeatEachIndex}`);
9544
9689
  return `${serverUrl.replace(/\/+$/, "")}/test-runs/${runId}/locate?${params.join("&")}`;
9545
9690
  }
9546
9691
  function formatFailureLine(link) {
@@ -9594,7 +9739,7 @@ function browserFunctionsInWorker() {
9594
9739
  function browserFunctionCoverage(files) {
9595
9740
  return sanitizeCoverage({
9596
9741
  v: 1,
9597
- files: [...files].map(([path32, functions]) => ({ path: path32, runtime: "browser", functions }))
9742
+ files: [...files].map(([path34, functions]) => ({ path: path34, runtime: "browser", functions }))
9598
9743
  });
9599
9744
  }
9600
9745
  function newKnownFunctions(known, sent) {
@@ -9607,19 +9752,19 @@ function newKnownFunctions(known, sent) {
9607
9752
  budget = MAX_COVERAGE_RANGES;
9608
9753
  };
9609
9754
  for (const [raw, ranges] of known) {
9610
- const path32 = normalizeCoveragePath(raw);
9611
- if (path32 === null) continue;
9612
- let named = sent.get(path32);
9755
+ const path34 = normalizeCoveragePath(raw);
9756
+ if (path34 === null) continue;
9757
+ let named = sent.get(path34);
9613
9758
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
9614
9759
  if (fresh.length === 0) continue;
9615
- if (!named) sent.set(path32, named = /* @__PURE__ */ new Set());
9760
+ if (!named) sent.set(path34, named = /* @__PURE__ */ new Set());
9616
9761
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
9617
9762
  while (fresh.length > 0) {
9618
9763
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
9619
9764
  const part = fresh.slice(0, budget);
9620
9765
  fresh = fresh.slice(part.length);
9621
9766
  budget -= part.length;
9622
- files.push({ path: path32, allFunctions: part });
9767
+ files.push({ path: path34, allFunctions: part });
9623
9768
  }
9624
9769
  }
9625
9770
  close();
@@ -9631,8 +9776,8 @@ function serverCoverageInWorker() {
9631
9776
 
9632
9777
  // src/internal/capture/code-reach.ts
9633
9778
  var crypto5 = __toESM(require("crypto"));
9634
- var fs22 = __toESM(require("fs"));
9635
- var path22 = __toESM(require("path"));
9779
+ var fs23 = __toESM(require("fs"));
9780
+ var path23 = __toESM(require("path"));
9636
9781
  var import_node_child_process3 = require("child_process");
9637
9782
 
9638
9783
  // ../core/src/source-map.ts
@@ -9977,18 +10122,18 @@ function repositoryRoot(from) {
9977
10122
  }).trim();
9978
10123
  } catch {
9979
10124
  }
9980
- cachedRepoRoot = { from, root: path22.resolve(root) };
10125
+ cachedRepoRoot = { from, root: path23.resolve(root) };
9981
10126
  return cachedRepoRoot.root;
9982
10127
  }
9983
10128
  function codeReachRoots(configDir, configured) {
9984
10129
  const repoRoot = repositoryRoot(configDir);
9985
- const resolve9 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path22.resolve(configDir, r));
9986
- const global = Array.isArray(configured) ? resolve9(configured) : resolve9(configured?.["*"]);
10130
+ const resolve11 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path23.resolve(configDir, r));
10131
+ const global = Array.isArray(configured) ? resolve11(configured) : resolve11(configured?.["*"]);
9987
10132
  const byOrigin = /* @__PURE__ */ new Map();
9988
10133
  if (configured && !Array.isArray(configured)) {
9989
10134
  for (const [key, dirs] of Object.entries(configured)) {
9990
10135
  const origin = key === "*" ? null : originOf3(key);
9991
- const resolved = resolve9(dirs);
10136
+ const resolved = resolve11(dirs);
9992
10137
  if (origin && resolved.length) byOrigin.set(origin, resolved);
9993
10138
  }
9994
10139
  }
@@ -10011,7 +10156,7 @@ function scriptRoots(roots, scriptUrl, pageUrl) {
10011
10156
  }
10012
10157
  function isFile(file) {
10013
10158
  try {
10014
- return fs22.statSync(file).isFile();
10159
+ return fs23.statSync(file).isFile();
10015
10160
  } catch {
10016
10161
  return false;
10017
10162
  }
@@ -10019,10 +10164,10 @@ function isFile(file) {
10019
10164
  function resolveSourcePath(source, roots) {
10020
10165
  const normalized = normalizeSourcePath(source);
10021
10166
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
10022
- if (path22.isAbsolute(normalized) && isFile(normalized)) return path22.resolve(normalized);
10167
+ if (path23.isAbsolute(normalized) && isFile(normalized)) return path23.resolve(normalized);
10023
10168
  const relative11 = normalized.replace(/^\/+/, "");
10024
10169
  for (const root of roots) {
10025
- const candidate = path22.resolve(root, relative11);
10170
+ const candidate = path23.resolve(root, relative11);
10026
10171
  if (isFile(candidate)) return candidate;
10027
10172
  }
10028
10173
  return null;
@@ -10054,7 +10199,7 @@ function fileForUrl(url, roots, bases) {
10054
10199
  return file;
10055
10200
  }
10056
10201
  function relativeToRepo(file, repoRoot) {
10057
- return path22.relative(repoRoot, file).split(path22.sep).join("/");
10202
+ return path23.relative(repoRoot, file).split(path23.sep).join("/");
10058
10203
  }
10059
10204
  function scriptVersion(entry) {
10060
10205
  return entry.hash ?? crypto5.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -10111,10 +10256,10 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
10111
10256
  const files = /* @__PURE__ */ new Map();
10112
10257
  const known = /* @__PURE__ */ new Map();
10113
10258
  const addTo = (into, file, functions) => {
10114
- const path32 = relativeToRepo(file, roots.repoRoot);
10115
- const listed = into.get(path32);
10259
+ const path34 = relativeToRepo(file, roots.repoRoot);
10260
+ const listed = into.get(path34);
10116
10261
  if (listed) listed.push(...functions);
10117
- else into.set(path32, [...functions]);
10262
+ else into.set(path34, [...functions]);
10118
10263
  };
10119
10264
  const add = (file, functions) => addTo(files, file, functions);
10120
10265
  const bases = viteBases(entries);
@@ -10266,8 +10411,8 @@ function onScriptParsed(page, capture, event) {
10266
10411
  var READ_WAIT_MS = 5e3;
10267
10412
  function readCoverage(page, cdp) {
10268
10413
  let timer;
10269
- const late = new Promise((resolve9) => {
10270
- timer = setTimeout(() => resolve9([]), READ_WAIT_MS);
10414
+ const late = new Promise((resolve11) => {
10415
+ timer = setTimeout(() => resolve11([]), READ_WAIT_MS);
10271
10416
  });
10272
10417
  const taken = internalCall(page, () => cdp.send("Profiler.takePreciseCoverage")).then(
10273
10418
  (answer) => answer.result,
@@ -10379,8 +10524,8 @@ async function stopCodeReach(page) {
10379
10524
  // src/internal/collect/blob-ids.ts
10380
10525
  var import_node_child_process4 = require("child_process");
10381
10526
  var crypto6 = __toESM(require("crypto"));
10382
- var fs23 = __toESM(require("fs"));
10383
- var path23 = __toESM(require("path"));
10527
+ var fs24 = __toESM(require("fs"));
10528
+ var path24 = __toESM(require("path"));
10384
10529
  function gitBlobId(content) {
10385
10530
  return crypto6.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
10386
10531
  }
@@ -10389,14 +10534,14 @@ function namesLines(file) {
10389
10534
  }
10390
10535
  var GIT_BATCH = 500;
10391
10536
  function gitHashObjects(root, paths) {
10392
- return new Promise((resolve9) => {
10537
+ return new Promise((resolve11) => {
10393
10538
  const child = (0, import_node_child_process4.execFile)(
10394
10539
  "git",
10395
10540
  ["hash-object", "--stdin-paths"],
10396
10541
  { cwd: root, encoding: "utf8", timeout: 3e4, windowsHide: true },
10397
10542
  (error, stdout) => {
10398
10543
  const ids = error ? [] : stdout.split("\n").filter(Boolean);
10399
- resolve9(ids.length === paths.length ? ids : null);
10544
+ resolve11(ids.length === paths.length ? ids : null);
10400
10545
  }
10401
10546
  );
10402
10547
  child.stdin?.on("error", () => {
@@ -10447,15 +10592,15 @@ var BlobIds = class {
10447
10592
  async read(relativePaths) {
10448
10593
  let root;
10449
10594
  try {
10450
- root = path23.resolve(this.repoRoot());
10595
+ root = path24.resolve(this.repoRoot());
10451
10596
  } catch {
10452
10597
  return relativePaths.map(() => null);
10453
10598
  }
10454
10599
  const files = await Promise.all(
10455
10600
  relativePaths.map(async (relativePath2) => {
10456
- const file = path23.resolve(root, relativePath2);
10457
- if (!file.startsWith(root + path23.sep)) return null;
10458
- const stat = await fs23.promises.stat(file).catch(() => null);
10601
+ const file = path24.resolve(root, relativePath2);
10602
+ if (!file.startsWith(root + path24.sep)) return null;
10603
+ const stat = await fs24.promises.stat(file).catch(() => null);
10459
10604
  return stat?.isFile() ? file : null;
10460
10605
  })
10461
10606
  );
@@ -10463,7 +10608,7 @@ var BlobIds = class {
10463
10608
  const fromGit = /* @__PURE__ */ new Map();
10464
10609
  const ids = forGit.length > 0 ? await this.hashObjects(
10465
10610
  root,
10466
- forGit.map((file) => path23.relative(root, file))
10611
+ forGit.map((file) => path24.relative(root, file))
10467
10612
  ) : null;
10468
10613
  if (ids) forGit.forEach((file, i) => fromGit.set(file, ids[i]));
10469
10614
  return Promise.all(
@@ -10472,7 +10617,7 @@ var BlobIds = class {
10472
10617
  const id = fromGit.get(file);
10473
10618
  if (id) return id;
10474
10619
  try {
10475
- return gitBlobId(await fs23.promises.readFile(file));
10620
+ return gitBlobId(await fs24.promises.readFile(file));
10476
10621
  } catch {
10477
10622
  return null;
10478
10623
  }
@@ -10669,7 +10814,7 @@ var CoverageCollection = class {
10669
10814
  }
10670
10815
  let timer;
10671
10816
  let finish;
10672
- const done = new Promise((resolve9) => {
10817
+ const done = new Promise((resolve11) => {
10673
10818
  let settled = false;
10674
10819
  finish = (server) => {
10675
10820
  if (settled) return;
@@ -10678,7 +10823,7 @@ var CoverageCollection = class {
10678
10823
  this.pending.delete(entry);
10679
10824
  void Promise.resolve(browser).then((files) => {
10680
10825
  this.assign(testCase, composeTestCoverage(server, files, this.options.settings));
10681
- resolve9();
10826
+ resolve11();
10682
10827
  });
10683
10828
  };
10684
10829
  });
@@ -10710,8 +10855,8 @@ var CoverageCollection = class {
10710
10855
  let timer;
10711
10856
  await Promise.race([
10712
10857
  Promise.all(waiting),
10713
- new Promise((resolve9) => {
10714
- timer = setTimeout(resolve9, boundMs);
10858
+ new Promise((resolve11) => {
10859
+ timer = setTimeout(resolve11, boundMs);
10715
10860
  })
10716
10861
  ]);
10717
10862
  clearTimeout(timer);
@@ -10829,10 +10974,13 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10829
10974
  this.streamManager = null;
10830
10975
  /** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
10831
10976
  this.setupSteps = [];
10832
- this.options = resolveOptions(rawOptions);
10977
+ const configDir = typeof rawOptions.configDir === "string" ? rawOptions.configDir : void 0;
10978
+ this.options = resolveOptions(rawOptions, configDir);
10833
10979
  this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
10834
10980
  this.listMode = isListMode();
10835
10981
  this.viaDesktopApp = usedDesktopDiscovery();
10982
+ this.desktopReplaced = desktopReplacedServer();
10983
+ this.viaDotEnv = usedDotEnv();
10836
10984
  this.coverageSettings = coverageSettings(this.options);
10837
10985
  const logger = new Logger(this.options.verbose ?? false);
10838
10986
  this.logger = logger;
@@ -10890,14 +11038,17 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10890
11038
  return;
10891
11039
  }
10892
11040
  if (this.viaDesktopApp) {
10893
- this.logger.info(`Using the desktop app running at ${this.options.serverUrl} (no serverUrl configured).`);
11041
+ this.logger.info(
11042
+ this.desktopReplaced ? `Using the desktop app running at ${this.options.serverUrl} instead of ${this.desktopReplaced} (desktopApp: 'prefer', not on CI).` : `Using the desktop app running at ${this.options.serverUrl} (no serverUrl configured).`
11043
+ );
10894
11044
  }
11045
+ if (this.viaDotEnv) this.logger.debug(`Read ${this.viaDotEnv.keys.join(", ")} from ${this.viaDotEnv.file}.`);
10895
11046
  this.startTime = (/* @__PURE__ */ new Date()).toISOString();
10896
11047
  this.testPathBase = configDirFromConfig(config);
10897
11048
  this.playwrightVersion = config.version;
10898
11049
  this.maxFailures = config.maxFailures ?? 0;
10899
11050
  if (this.options.captureResources !== false) {
10900
- this.resourcesFile = path24.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
11051
+ this.resourcesFile = path25.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
10901
11052
  process.env[PIWI_RESOURCES_RESULTS_ENV] = this.resourcesFile;
10902
11053
  try {
10903
11054
  this.sampler = new RunSampler({ outputDirs: (config.projects ?? []).map((project) => project.outputDir) });
@@ -11039,6 +11190,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11039
11190
  type: "begin",
11040
11191
  title: test.title,
11041
11192
  location: testLocation(test, this.testPathBase),
11193
+ repeatEachIndex: test.repeatEachIndex,
11042
11194
  workerIndex: workerIndexOf(result),
11043
11195
  shardIndex: this.shardInfo?.current ?? null,
11044
11196
  browser: this.metadataCollector.getBrowserConfig(test) || void 0,
@@ -11146,6 +11298,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11146
11298
  // Playwright reports the same line, so synthesize it.
11147
11299
  error: expectedFailureError(result.status, annotations) ?? buildErrorText(result, this.testPathBase),
11148
11300
  retries: result.retry,
11301
+ // Each `--repeat-each` repetition is a test of its own, with its own attempts.
11302
+ repeatEachIndex: test.repeatEachIndex,
11149
11303
  attempts: attempts.map((a) => ({ ...a })),
11150
11304
  workerIndex: workerIndexOf(result),
11151
11305
  shardIndex: this.shardInfo?.current ?? null,
@@ -11172,10 +11326,11 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11172
11326
  if (frames.length) testCase.testSourceFrames = frames;
11173
11327
  }
11174
11328
  if (this.options.collectPerformanceMetrics && result.steps?.length > 0) {
11175
- testCase.performanceMetrics = collectStepMetrics(result.steps, result.errors);
11329
+ const pauses = readPauses(result.attachments?.find((a) => a.name === ATTACHMENT_NAMES.pauses)?.body);
11330
+ testCase.performanceMetrics = collectStepMetrics(result.steps, result.errors, pauses);
11176
11331
  const stepEvents = extractTestStepEvents(result.steps, result.startTime);
11177
11332
  const waitEvents = extractWaitEvents(result.steps);
11178
- const allEvents = [...stepEvents, ...waitEvents];
11333
+ const allEvents = [...stepEvents, ...pauseStepEvents(pauses), ...waitEvents];
11179
11334
  if (allEvents.length > 0) testCase.stepEvents = allEvents;
11180
11335
  }
11181
11336
  if (this.options.collectPerformanceMetrics && result.attachments) {
@@ -11232,6 +11387,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11232
11387
  file: testFile(test, this.testPathBase),
11233
11388
  retry: result.retry,
11234
11389
  browser: typeof testCase.browser?.projectName === "string" ? testCase.browser.projectName : null,
11390
+ repeatEachIndex: test.repeatEachIndex,
11235
11391
  headline: failureHeadline(testCase.error, testCase.performanceMetrics?.steps)
11236
11392
  });
11237
11393
  }
@@ -11262,7 +11418,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11262
11418
  if (!file.path || INTERNAL_ATTACHMENT_NAMES.has(file.name)) continue;
11263
11419
  try {
11264
11420
  const kind = artifactKind(file.name, file.contentType);
11265
- const bytes = fs24.statSync(file.path).size;
11421
+ const bytes = fs25.statSync(file.path).size;
11266
11422
  artifacts[kind] += bytes;
11267
11423
  this.artifactBytes[kind] += bytes;
11268
11424
  } catch {
@@ -11295,8 +11451,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11295
11451
  const censuses = [...this.resourceCensuses];
11296
11452
  if (this.resourcesFile) {
11297
11453
  try {
11298
- censuses.push(...parseResourceResults(fs24.readFileSync(this.resourcesFile, "utf8")));
11299
- fs24.rmSync(this.resourcesFile, { force: true });
11454
+ censuses.push(...parseResourceResults(fs25.readFileSync(this.resourcesFile, "utf8")));
11455
+ fs25.rmSync(this.resourcesFile, { force: true });
11300
11456
  } catch {
11301
11457
  }
11302
11458
  }
@@ -11350,6 +11506,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11350
11506
  timeout: test.timeout ?? null,
11351
11507
  error: null,
11352
11508
  retries: 0,
11509
+ repeatEachIndex: test.repeatEachIndex,
11353
11510
  workerIndex: null,
11354
11511
  shardIndex: this.shardInfo?.current ?? null,
11355
11512
  startedAt: null,
@@ -11520,7 +11677,7 @@ async function resolveSelection(options = {}) {
11520
11677
  }
11521
11678
 
11522
11679
  // src/internal/capture/capture-fixtures.ts
11523
- var path28 = __toESM(require("path"));
11680
+ var path30 = __toESM(require("path"));
11524
11681
  var import_node_zlib2 = require("zlib");
11525
11682
 
11526
11683
  // ../picker-dom/src/probe.ts
@@ -11874,10 +12031,11 @@ function installPickerOverlay(arg) {
11874
12031
  }
11875
12032
  const Z = 2147483600;
11876
12033
  const testIdAttr = arg.testIdAttribute || "data-testid";
12034
+ const enterReaches = arg.transport === "global";
11877
12035
  const words = {
11878
12036
  banner: "Piwi inspector \u2014 click any element to generate locators for it",
11879
- keys: "\u2191 parent \xB7 \u2193 child \xB7 Esc skip",
11880
- keysHovering: "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip",
12037
+ keys: enterReaches ? "Enter picks the focused element \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip" : "\u2191 parent \xB7 \u2193 child \xB7 Esc skip",
12038
+ keysHovering: enterReaches ? "click or Enter to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip" : "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip",
11881
12039
  analyzing: "Analyzing element\u2026"
11882
12040
  };
11883
12041
  for (const key of Object.keys(words)) {
@@ -11972,6 +12130,16 @@ function installPickerOverlay(arg) {
11972
12130
  const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
11973
12131
  return cls ? `locator(${quoteJs(`.${cls}`)})` : tag;
11974
12132
  };
12133
+ const parentOf = (node) => {
12134
+ if (node.parentElement) return node.parentElement;
12135
+ const root = node.getRootNode ? node.getRootNode() : null;
12136
+ return root && root !== doc && root.host ? root.host : null;
12137
+ };
12138
+ const targetOf = (e) => {
12139
+ const path34 = typeof e.composedPath === "function" ? e.composedPath() : null;
12140
+ const first = path34 && path34.length ? path34[0] : null;
12141
+ return first && first.nodeType === 1 ? first : e.target;
12142
+ };
11975
12143
  const buildChain = (raw) => {
11976
12144
  const chain2 = [];
11977
12145
  let node = raw;
@@ -11979,7 +12147,7 @@ function installPickerOverlay(arg) {
11979
12147
  const tag = (node.tagName || "").toLowerCase();
11980
12148
  if (tag === "body" || tag === "html") break;
11981
12149
  chain2.push(node);
11982
- node = node.parentElement;
12150
+ node = parentOf(node);
11983
12151
  }
11984
12152
  return chain2.length ? chain2 : [raw];
11985
12153
  };
@@ -12070,10 +12238,14 @@ function installPickerOverlay(arg) {
12070
12238
  }
12071
12239
  };
12072
12240
  const onMove = (e) => {
12073
- const raw = e.target;
12241
+ const raw = targetOf(e);
12074
12242
  if (!raw || isOwn(raw)) {
12243
+ chain = [];
12244
+ idx = 0;
12245
+ lastRaw = null;
12075
12246
  highlight.style.display = "none";
12076
12247
  label.style.display = "none";
12248
+ foot.textContent = words.keys;
12077
12249
  return;
12078
12250
  }
12079
12251
  if (raw !== lastRaw) {
@@ -12152,17 +12324,30 @@ function installPickerOverlay(arg) {
12152
12324
  const onClick = (e) => {
12153
12325
  stop(e);
12154
12326
  const el = current();
12155
- if (!el || isOwn(e.target)) return;
12327
+ if (!el || isOwn(targetOf(e))) return;
12156
12328
  reportPicked(el);
12157
12329
  };
12330
+ const focused = () => {
12331
+ let el = doc.activeElement;
12332
+ while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement;
12333
+ const tag = (el && el.tagName || "").toLowerCase();
12334
+ return !el || tag === "body" || tag === "html" || isOwn(el) ? null : el;
12335
+ };
12158
12336
  const onKey = (e) => {
12159
12337
  if (e.key === "Escape") {
12160
12338
  stop(e);
12161
12339
  reportSkipped();
12162
12340
  return;
12163
12341
  }
12342
+ if (e.key === "Enter" && !e.isComposing) {
12343
+ stop(e);
12344
+ const el = current() || focused();
12345
+ if (el) reportPicked(el);
12346
+ return;
12347
+ }
12164
12348
  if (e.key === "ArrowUp" || e.key === "ArrowDown") {
12165
12349
  stop(e);
12350
+ if (!chain.length) return;
12166
12351
  if (e.key === "ArrowUp") idx = Math.min(idx + 1, chain.length - 1);
12167
12352
  else idx = Math.max(idx - 1, 0);
12168
12353
  refresh();
@@ -12236,12 +12421,43 @@ function showAnchorPicker(arg) {
12236
12421
  const al = n.getAttribute && n.getAttribute("aria-level");
12237
12422
  return al && /^\d+$/.test(al) ? Number(al) : null;
12238
12423
  };
12424
+ const parentOf = (n) => {
12425
+ if (n.parentElement) return n.parentElement;
12426
+ const root = n.getRootNode ? n.getRootNode() : null;
12427
+ return root && root !== doc && root.host ? root.host : null;
12428
+ };
12429
+ const shadowRoots = [];
12430
+ const collectRoots = (root) => {
12431
+ const all = root.querySelectorAll("*");
12432
+ for (let i = 0; i < all.length; i++) {
12433
+ const sr = all[i].shadowRoot;
12434
+ if (sr) {
12435
+ shadowRoots.push(sr);
12436
+ collectRoots(sr);
12437
+ }
12438
+ }
12439
+ };
12440
+ try {
12441
+ collectRoots(doc);
12442
+ } catch {
12443
+ shadowRoots.length = 0;
12444
+ }
12445
+ const within = (node2, scope) => {
12446
+ if (scope === doc) return true;
12447
+ for (let n = node2; n; n = parentOf(n)) if (n === scope) return true;
12448
+ return false;
12449
+ };
12450
+ const queryAll = (scope, sel) => {
12451
+ const found = Array.from(scope.querySelectorAll(sel));
12452
+ for (const sr of shadowRoots) if (within(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
12453
+ return found;
12454
+ };
12239
12455
  const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
12240
12456
  const leafMatches = (scope) => {
12241
12457
  try {
12242
- if (leafTestId) return scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(leafTestId)}]`).length;
12458
+ if (leafTestId) return queryAll(scope, `[${testIdAttr}=${JSON.stringify(leafTestId)}]`).length;
12243
12459
  if (!leafCountable) return -1;
12244
- const nodes = scope.querySelectorAll(roleSources);
12460
+ const nodes = queryAll(scope, roleSources);
12245
12461
  if (nodes.length > 2e3) return -1;
12246
12462
  let matched = 0;
12247
12463
  for (let i = 0; i < nodes.length; i++) {
@@ -12257,20 +12473,20 @@ function showAnchorPicker(arg) {
12257
12473
  };
12258
12474
  let roleNodes = [];
12259
12475
  try {
12260
- const all = doc.querySelectorAll(roleSources);
12261
- if (all.length <= 4e3) roleNodes = Array.from(all);
12476
+ const all = queryAll(doc, roleSources);
12477
+ if (all.length <= 4e3) roleNodes = all;
12262
12478
  } catch {
12263
12479
  roleNodes = [];
12264
12480
  }
12265
12481
  const count = (sel) => {
12266
12482
  try {
12267
- return doc.querySelectorAll(sel).length;
12483
+ return queryAll(doc, sel).length;
12268
12484
  } catch {
12269
12485
  return void 0;
12270
12486
  }
12271
12487
  };
12272
12488
  const rows = [];
12273
- let node = el.parentElement;
12489
+ let node = parentOf(el);
12274
12490
  let depth = 0;
12275
12491
  while (node && depth < 12) {
12276
12492
  depth++;
@@ -12307,7 +12523,7 @@ function showAnchorPicker(arg) {
12307
12523
  hookLabel,
12308
12524
  selectable: !!(testId || id || role && (ariaLabel || info.roleCount === 1))
12309
12525
  });
12310
- node = node.parentElement;
12526
+ node = parentOf(node);
12311
12527
  }
12312
12528
  if (rows.length === 0) {
12313
12529
  g.__piwiAnchorState = "skipped";
@@ -12336,9 +12552,9 @@ function showAnchorPicker(arg) {
12336
12552
  footer.style.cssText = "margin:10px 0;font-weight:600;";
12337
12553
  const segMatches = (scope, info) => {
12338
12554
  try {
12339
- if (info.testId) return Array.from(scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(info.testId)}]`));
12340
- if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
12341
- const nodes = Array.from(scope.querySelectorAll(roleSources));
12555
+ if (info.testId) return queryAll(scope, `[${testIdAttr}=${JSON.stringify(info.testId)}]`);
12556
+ if (info.id) return queryAll(scope, `#${doc.defaultView.CSS.escape(info.id)}`);
12557
+ const nodes = queryAll(scope, roleSources);
12342
12558
  if (nodes.length > 2e3) return [];
12343
12559
  return nodes.filter(
12344
12560
  (n) => roleOf2(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
@@ -12625,8 +12841,11 @@ function showPauseBar(arg) {
12625
12841
  };
12626
12842
  const buttons = [
12627
12843
  ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
12628
- ["Step", "step", "Pause again at the next action", false],
12844
+ ["Step", "step", "Pause again before the next call", false],
12629
12845
  ["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
12846
+ ...arg.record ? [
12847
+ ["Record here", "record", "Record steps in this page: the editor writes them before this line", false]
12848
+ ] : [],
12630
12849
  ["Finish", "finish", "Run on and pause no more in this test", false]
12631
12850
  ];
12632
12851
  for (const [label, choice, hint, primary] of buttons) {
@@ -13102,21 +13321,21 @@ var MAX_INDETERMINATE = 20;
13102
13321
  var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
13103
13322
  var LEVEL = `${STEP}(?:/${STEP})*`;
13104
13323
  var ELEMENT_PATH_RE = new RegExp(`^${LEVEL}(?:\\|${LEVEL})*$`);
13105
- function isElementPath(path32) {
13106
- return typeof path32 === "string" && path32.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path32);
13324
+ function isElementPath(path34) {
13325
+ return typeof path34 === "string" && path34.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path34);
13107
13326
  }
13108
13327
  var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
13109
13328
  function elementRef(v) {
13110
13329
  if (!v || typeof v !== "object") return null;
13111
- const { path: path32, label } = v;
13112
- if (!isElementPath(path32)) return null;
13113
- return { path: path32, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
13330
+ const { path: path34, label } = v;
13331
+ if (!isElementPath(path34)) return null;
13332
+ return { path: path34, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
13114
13333
  }
13115
13334
  function textPoint(v) {
13116
13335
  if (!v || typeof v !== "object") return null;
13117
- const { path: path32, offset } = v;
13336
+ const { path: path34, offset } = v;
13118
13337
  const at = offsetOf(offset);
13119
- return isElementPath(path32) && at !== null ? { path: path32, offset: at } : null;
13338
+ return isElementPath(path34) && at !== null ? { path: path34, offset: at } : null;
13120
13339
  }
13121
13340
  function sanitizePageInteraction(raw) {
13122
13341
  if (!raw || typeof raw !== "object") return {};
@@ -13147,7 +13366,7 @@ function sanitizePageInteraction(raw) {
13147
13366
  }
13148
13367
 
13149
13368
  // src/internal/capture/locator-healing.ts
13150
- var path25 = __toESM(require("path"));
13369
+ var path26 = __toESM(require("path"));
13151
13370
 
13152
13371
  // ../core/src/locator-fingerprint.ts
13153
13372
  var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
@@ -13532,10 +13751,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
13532
13751
  }
13533
13752
  let rel = file;
13534
13753
  try {
13535
- rel = path25.relative(process.cwd(), file);
13754
+ rel = path26.relative(process.cwd(), file);
13536
13755
  } catch {
13537
13756
  }
13538
- rel = rel.split(path25.sep).join("/");
13757
+ rel = rel.split(path26.sep).join("/");
13539
13758
  if (rel.startsWith("./")) rel = rel.slice(2);
13540
13759
  return `${rel}:${m[3]}:${m[4]}`;
13541
13760
  }
@@ -13765,7 +13984,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
13765
13984
  }
13766
13985
 
13767
13986
  // src/internal/capture/pick-on-failure.ts
13768
- var path26 = __toESM(require("path"));
13987
+ var path27 = __toESM(require("path"));
13769
13988
  function parseLeafLocatorExpression(rawExpr) {
13770
13989
  const leaf = parseLeafLocatorCall(rawExpr);
13771
13990
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -13780,37 +13999,60 @@ ${err.stack ?? ""}`));
13780
13999
  const parsed2 = parseLeafLocatorExpression(header);
13781
14000
  if (!parsed2) continue;
13782
14001
  const loc = err.location;
13783
- const location = loc ? `${path26.relative(process.cwd(), loc.file).split(path26.sep).join("/")}:${loc.line}:${loc.column}` : null;
14002
+ const location = loc ? `${path27.relative(process.cwd(), loc.file).split(path27.sep).join("/")}:${loc.line}:${loc.column}` : null;
13784
14003
  return { method: parsed2.method, args: parsed2.args, location };
13785
14004
  }
13786
14005
  return null;
13787
14006
  }
13788
- async function cleanupPicker(page) {
14007
+ var PICK_ENDED = "__piwiPickEnded";
14008
+ async function cleanupPicker(page, ended = false) {
13789
14009
  try {
13790
- await page.evaluate(() => {
13791
- const g = globalThis;
13792
- if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
13793
- if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
13794
- delete g.__piwiPickCleanup;
13795
- delete g.__piwiAnchorCleanup;
13796
- delete g.__piwiPickState;
13797
- delete g.__piwiPickChoice;
13798
- delete g.__piwiPickedElement;
13799
- delete g.__piwiAnchorState;
13800
- delete g.__piwiPickAnchors;
13801
- delete g.__piwiPickChainCount;
13802
- });
14010
+ await page.evaluate(
14011
+ ([endedKey, unanswered]) => {
14012
+ const g = globalThis;
14013
+ if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
14014
+ if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
14015
+ delete g.__piwiPickCleanup;
14016
+ delete g.__piwiAnchorCleanup;
14017
+ delete g.__piwiPickState;
14018
+ delete g.__piwiPickChoice;
14019
+ delete g.__piwiPickedElement;
14020
+ delete g.__piwiAnchorState;
14021
+ delete g.__piwiPickAnchors;
14022
+ delete g.__piwiPickChainCount;
14023
+ if (unanswered) g[endedKey] = true;
14024
+ else delete g[endedKey];
14025
+ },
14026
+ [PICK_ENDED, ended]
14027
+ );
13803
14028
  } catch {
13804
14029
  }
13805
14030
  }
13806
- async function pickLocator(page, probe, failing) {
14031
+ function unlessEnded(run, ended) {
14032
+ if (!ended) return run;
14033
+ return Promise.race([
14034
+ run,
14035
+ new Promise((resolve11) => {
14036
+ if (ended.aborted) resolve11(void 0);
14037
+ else ended.addEventListener("abort", () => resolve11(void 0), { once: true });
14038
+ })
14039
+ ]);
14040
+ }
14041
+ async function pickLocator(page, probe, failing, ended) {
14042
+ const answered = async (name) => {
14043
+ const wait = page.waitForFunction(
14044
+ ([key, endedKey]) => globalThis[key] !== void 0 || globalThis[endedKey] === true,
14045
+ [name, PICK_ENDED],
14046
+ { timeout: 0, polling: 250 }
14047
+ );
14048
+ await unlessEnded(wait, ended);
14049
+ return !ended?.aborted;
14050
+ };
13807
14051
  try {
13808
14052
  const overlayArg = { transport: "global", failing };
14053
+ await page.evaluate((endedKey) => delete globalThis[endedKey], PICK_ENDED);
13809
14054
  await page.evaluate(installPickerOverlay, overlayArg);
13810
- await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
13811
- timeout: 0,
13812
- polling: 250
13813
- });
14055
+ if (!await answered("__piwiPickState")) return null;
13814
14056
  const state = await page.evaluate(() => globalThis.__piwiPickState);
13815
14057
  if (state !== "picked") return null;
13816
14058
  const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
@@ -13831,10 +14073,7 @@ async function pickLocator(page, probe, failing) {
13831
14073
  leafLevel: level,
13832
14074
  leafTestId: attrs.attributes["data-testid"] ?? null
13833
14075
  });
13834
- await page.waitForFunction(() => globalThis.__piwiAnchorState !== void 0, void 0, {
13835
- timeout: 0,
13836
- polling: 250
13837
- });
14076
+ if (!await answered("__piwiAnchorState")) return null;
13838
14077
  const anchorResult = await page.evaluate(() => {
13839
14078
  const g = globalThis;
13840
14079
  return { state: g.__piwiAnchorState, anchors: g.__piwiPickAnchors ?? [], chainCount: g.__piwiPickChainCount };
@@ -13856,10 +14095,7 @@ async function pickLocator(page, probe, failing) {
13856
14095
  failing,
13857
14096
  choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
13858
14097
  });
13859
- await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
13860
- timeout: 0,
13861
- polling: 250
13862
- });
14098
+ if (!await answered("__piwiPickChoice")) return null;
13863
14099
  const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
13864
14100
  if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
13865
14101
  const picked = { ...ranked[choice], pickedByUser: true };
@@ -13878,7 +14114,7 @@ async function pickLocator(page, probe, failing) {
13878
14114
  ...anchors.length > 0 ? { anchors } : {}
13879
14115
  };
13880
14116
  } finally {
13881
- await cleanupPicker(page);
14117
+ await cleanupPicker(page, ended?.aborted);
13882
14118
  }
13883
14119
  }
13884
14120
  async function runLocatorPicker(page, testInfo, failed, probe) {
@@ -13933,7 +14169,7 @@ function applyPickToSnapshots(snapshots, pick) {
13933
14169
  }
13934
14170
 
13935
14171
  // src/internal/capture/pause-at.ts
13936
- var path27 = __toESM(require("path"));
14172
+ var path28 = __toESM(require("path"));
13937
14173
 
13938
14174
  // ../core/src/editor-send.ts
13939
14175
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
@@ -13974,18 +14210,32 @@ var PauseSet = class {
13974
14210
  return col > 0 && this.lines.has(callerLocation.slice(0, col));
13975
14211
  }
13976
14212
  };
13977
- var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
14213
+ var NO_BREAKPOINTS = new PauseSet(/* @__PURE__ */ new Set());
13978
14214
  function callSiteFile(file, cwd) {
13979
14215
  const slashed = file.replace(/\\/g, "/");
13980
- const absolute = path27.isAbsolute(file) || path27.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
13981
- let rel = absolute ? path27.relative(cwd, file) : path27.normalize(slashed);
13982
- rel = rel.split(path27.sep).join("/").replace(/\\/g, "/");
14216
+ const absolute = path28.isAbsolute(file) || path28.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
14217
+ let rel = absolute ? path28.relative(cwd, file) : path28.normalize(slashed);
14218
+ rel = rel.split(path28.sep).join("/").replace(/\\/g, "/");
13983
14219
  if (rel.startsWith("./")) rel = rel.slice(2);
13984
14220
  return rel;
13985
14221
  }
14222
+ function stopCallSite(location, cwd = process.cwd()) {
14223
+ if (!location.line) return null;
14224
+ return `${callSiteFile(location.file, cwd)}:${location.line}:${location.column ?? 0}`;
14225
+ }
14226
+ function callSiteLine(callSite) {
14227
+ const col = callSite.lastIndexOf(":");
14228
+ return col > 0 ? callSite.slice(0, col) : callSite;
14229
+ }
14230
+ function inFlightCallSite(inFlight) {
14231
+ const [first] = inFlight;
14232
+ if (first === void 0) return null;
14233
+ const line = callSiteLine(first);
14234
+ return inFlight.every((site) => callSiteLine(site) === line) ? first : null;
14235
+ }
13986
14236
  var ignoredLogged = /* @__PURE__ */ new Set();
13987
14237
  function parsePauseAt(value, cwd = process.cwd()) {
13988
- if (!value?.trim()) return EMPTY;
14238
+ if (!value?.trim()) return NO_BREAKPOINTS;
13989
14239
  const lines = /* @__PURE__ */ new Set();
13990
14240
  for (const raw of value.split(";")) {
13991
14241
  const entry = raw.trim();
@@ -14001,7 +14251,7 @@ function parsePauseAt(value, cwd = process.cwd()) {
14001
14251
  }
14002
14252
  lines.add(`${callSiteFile(m[1], cwd)}:${line}`);
14003
14253
  }
14004
- return lines.size ? new PauseSet(lines) : EMPTY;
14254
+ return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
14005
14255
  }
14006
14256
  var parsed = null;
14007
14257
  function currentPauseSet() {
@@ -14011,27 +14261,121 @@ function currentPauseSet() {
14011
14261
  }
14012
14262
  function pauseGateFromTestInfo(testInfo) {
14013
14263
  const use = testInfo.project?.use ?? {};
14014
- return { ci: process.env.CI, headless: use.headless };
14264
+ const debug = testInfo._configInternal?.configCLIOverrides?.debug;
14265
+ return { ci: process.env.CI, headless: use.headless, pwdebug: process.env.PWDEBUG, debug };
14015
14266
  }
14016
14267
  function pauseSkipReason(gate) {
14017
14268
  if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
14018
14269
  if (gate.headless !== false)
14019
14270
  return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
14271
+ if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug))
14272
+ return "Playwright's Inspector runs the test (PWDEBUG) \u2014 run without --debug";
14273
+ if (typeof gate.debug === "string" && gate.debug)
14274
+ return `Playwright's debugger runs the test (--debug=${gate.debug}) \u2014 run without --debug`;
14020
14275
  return null;
14021
14276
  }
14022
- function rearmedTimeout(before, pausedMs) {
14023
- return before > 0 ? before + Math.max(0, Math.round(pausedMs)) : 0;
14277
+ function atStop(state, callSite) {
14278
+ if (callSite && !state.finished) {
14279
+ if (state.step) {
14280
+ state.step = false;
14281
+ return { show: "step" };
14282
+ }
14283
+ if (state.set.matches(callSite)) return { show: "breakpoint" };
14284
+ }
14285
+ return { go: goOn(state) };
14286
+ }
14287
+ function goOn(state, answer) {
14288
+ if (answer === "step") state.step = true;
14289
+ else if (answer === "finish") state.finished = true;
14290
+ return !state.finished && (state.step || state.set.size > 0) ? "next" : "resume";
14291
+ }
14292
+ function contextDebugger(context) {
14293
+ const dbg = context.debugger;
14294
+ return typeof dbg?.requestPause === "function" && typeof dbg.on === "function" ? dbg : null;
14295
+ }
14296
+ var WATCHED = /* @__PURE__ */ new WeakSet();
14297
+ function watchStops(context, onStop) {
14298
+ const dbg = contextDebugger(context);
14299
+ if (!dbg) return false;
14300
+ if (WATCHED.has(context)) return true;
14301
+ WATCHED.add(context);
14302
+ inspectorSwitch.watching = true;
14303
+ syncInspectorSwitch();
14304
+ dbg.on("pausedstatechanged", () => onStop(context));
14305
+ return true;
14306
+ }
14307
+ var NO_INSPECTOR_ENV = "PW_CODEGEN_NO_INSPECTOR";
14308
+ var inspectorSwitch = {
14309
+ /** Set by Piwi, so Piwi's to clear. */
14310
+ set: false,
14311
+ /** Piwi answers the stops of a context in this worker. */
14312
+ watching: false,
14313
+ /** Every `page.pause()` of the worker's Playwright reaches it through Piwi (`markPausesWrapped`). */
14314
+ wrapped: false,
14315
+ /** The `page.pause()` calls left to Playwright that are running (`withInspector`). */
14316
+ open: 0
14317
+ };
14318
+ function syncInspectorSwitch() {
14319
+ const closed = inspectorSwitch.watching && inspectorSwitch.wrapped && inspectorSwitch.open === 0;
14320
+ if (closed && process.env[NO_INSPECTOR_ENV] === void 0) {
14321
+ process.env[NO_INSPECTOR_ENV] = "1";
14322
+ inspectorSwitch.set = true;
14323
+ } else if (!closed && inspectorSwitch.set) {
14324
+ if (process.env[NO_INSPECTOR_ENV] === "1") delete process.env[NO_INSPECTOR_ENV];
14325
+ inspectorSwitch.set = false;
14326
+ }
14327
+ }
14328
+ function markPausesWrapped() {
14329
+ inspectorSwitch.wrapped = true;
14330
+ syncInspectorSwitch();
14331
+ }
14332
+ async function withInspector(run) {
14333
+ inspectorSwitch.open++;
14334
+ syncInspectorSwitch();
14335
+ try {
14336
+ return await run();
14337
+ } finally {
14338
+ inspectorSwitch.open--;
14339
+ syncInspectorSwitch();
14340
+ }
14341
+ }
14342
+ async function steerDebugger(context, how) {
14343
+ const dbg = contextDebugger(context);
14344
+ if (!dbg) return;
14345
+ try {
14346
+ await internalCall(
14347
+ context,
14348
+ () => how === "pause" ? dbg.requestPause() : how === "next" ? dbg.next() : dbg.resume()
14349
+ );
14350
+ } catch {
14351
+ }
14352
+ }
14353
+ async function withTestTimeoutHeld(testInfo, run) {
14354
+ const set = testInfo._setIgnoreTimeouts;
14355
+ if (typeof set !== "function") return run();
14356
+ set.call(testInfo, true);
14357
+ try {
14358
+ return await run();
14359
+ } finally {
14360
+ set.call(testInfo, false);
14361
+ }
14362
+ }
14363
+ function testTimeSlot(testInfo) {
14364
+ try {
14365
+ const slot = testInfo._timeoutManager?.defaultSlot?.();
14366
+ return slot && typeof slot.elapsed === "number" ? slot : null;
14367
+ } catch {
14368
+ return null;
14369
+ }
14370
+ }
14371
+ function restoreTestTime(testInfo, heldAt) {
14372
+ const slot = testTimeSlot(testInfo);
14373
+ if (slot && Number.isFinite(heldAt) && heldAt >= 0 && heldAt < slot.elapsed) slot.elapsed = heldAt;
14024
14374
  }
14025
14375
  function pausePlace(callerLocation) {
14026
- const col = callerLocation.lastIndexOf(":");
14027
- const fileLine = col > 0 ? callerLocation.slice(0, col) : callerLocation;
14376
+ const fileLine = callSiteLine(callerLocation);
14028
14377
  return fileLine.slice(fileLine.lastIndexOf("/") + 1);
14029
14378
  }
14030
- function assertionLabel(expression, isNot) {
14031
- const [first = "", ...rest] = expression.split(".").filter(Boolean);
14032
- const name = first + rest.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
14033
- return `${isNot ? "not." : ""}${name || "expect"}`;
14034
- }
14035
14379
  function pickPlace(callerLocation) {
14036
14380
  const m = /^(.+):(\d+):\d+$/.exec(callerLocation);
14037
14381
  if (!m) return null;
@@ -14076,59 +14420,312 @@ function logPauseSkipOnce(reason) {
14076
14420
  skipLogged = true;
14077
14421
  console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt} is set but breakpoints are skipped: ${reason}`);
14078
14422
  }
14423
+ var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
14424
+ var MAX_NAVIGATED = 10;
14079
14425
  var HIGHLIGHT_MS = 2e3;
14080
- async function pauseHere(page, testInfo, point, probe, onPick) {
14081
- const before = testInfo.timeout;
14082
- const started = Date.now();
14426
+ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
14083
14427
  let shown = null;
14084
14428
  try {
14085
- testInfo.setTimeout(0);
14086
14429
  const highlight = point.target?.highlight;
14087
14430
  if (typeof highlight === "function") {
14088
14431
  let timer;
14089
14432
  shown = await Promise.race([
14090
14433
  Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
14091
- new Promise((resolve9) => {
14092
- timer = setTimeout(() => resolve9(null), HIGHLIGHT_MS);
14434
+ new Promise((resolve11) => {
14435
+ timer = setTimeout(() => resolve11(null), HIGHLIGHT_MS);
14093
14436
  })
14094
14437
  ]);
14095
14438
  clearTimeout(timer);
14096
14439
  }
14097
14440
  const arg = {
14098
14441
  place: pausePlace(point.location),
14099
- attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
14100
- action: point.action,
14101
- locator: point.locator
14442
+ attempt: point.attempt,
14443
+ action: point.call,
14444
+ locator: null,
14445
+ record: !!onRecord
14102
14446
  };
14103
14447
  console.log(
14104
- `[piwi] Paused at ${point.location} before ${point.locator ? `${point.action} \xB7 ${point.locator}` : point.action}: Resume, Step, Pick a locator or Finish in the browser (Esc resumes).`
14448
+ `[piwi] Paused at ${callSiteLine(point.location)} before ${point.call}: Resume, Step, Pick a locator${onRecord ? ", Record here" : ""} or Finish in the browser (Esc resumes).`
14105
14449
  );
14450
+ let navigated = 0;
14106
14451
  for (; ; ) {
14107
- await page.evaluate(showPauseBar, arg);
14108
- await page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 });
14109
- const choice = await page.evaluate(takePauseState);
14110
- if (choice === "resume" || choice === "step" || choice === "finish") return choice;
14111
- if (choice !== "pick") continue;
14112
- const picked = await pickLocator(page, probe, null).catch(() => null);
14113
- if (picked) await onPick(picked);
14452
+ try {
14453
+ if (ended?.aborted) return "resume";
14454
+ await page.evaluate(showPauseBar, arg);
14455
+ await unlessEnded(page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 }), ended);
14456
+ if (ended?.aborted) return "resume";
14457
+ const choice = await page.evaluate(takePauseState);
14458
+ navigated = 0;
14459
+ if (choice === "resume" || choice === "step" || choice === "finish") return choice;
14460
+ if (choice === "record" && onRecord) {
14461
+ await unlessEnded(
14462
+ onRecord(ended).catch(() => void 0),
14463
+ ended
14464
+ );
14465
+ continue;
14466
+ }
14467
+ if (choice !== "pick") continue;
14468
+ const picked = await pickLocator(page, probe, null, ended).catch(() => null);
14469
+ if (picked && !ended?.aborted) await onPick(picked);
14470
+ } catch (error) {
14471
+ if (page.isClosed() || !NAVIGATED.test(String(error)) || ++navigated > MAX_NAVIGATED) throw error;
14472
+ await page.waitForLoadState("domcontentloaded").catch(() => void 0);
14473
+ }
14114
14474
  }
14115
14475
  } catch {
14116
14476
  return "resume";
14117
14477
  } finally {
14118
14478
  try {
14479
+ if (ended?.aborted) await page.evaluate(takePauseState);
14119
14480
  const dispose = shown?.dispose;
14120
14481
  const hide = page.hideHighlight;
14121
14482
  if (typeof dispose === "function") await dispose.call(shown);
14122
14483
  else if (typeof hide === "function") await hide.call(page);
14123
14484
  } catch {
14124
14485
  }
14125
- try {
14126
- testInfo.setTimeout(rearmedTimeout(before, Date.now() - started));
14127
- } catch {
14128
- }
14129
14486
  }
14130
14487
  }
14131
14488
 
14489
+ // src/internal/capture/record-handoff.ts
14490
+ var fs26 = __toESM(require("fs"));
14491
+ var net = __toESM(require("net"));
14492
+ var path29 = __toESM(require("path"));
14493
+
14494
+ // ../core/src/ide-recorder.ts
14495
+ var IDE_RECORDER_BINDING = "__piwiRecorder";
14496
+ var IDE_DISPATCH_GLOBAL = "__piwiIdeDispatch";
14497
+ var RECORD_CHANNEL_ENV = "PIWI_RECORD_CHANNEL";
14498
+ var MAX_HANDOFF_LINE = 64e6;
14499
+ function parseRecordChannel(value) {
14500
+ const text = value?.trim() ?? "";
14501
+ const hash = text.lastIndexOf("#");
14502
+ if (hash <= 0) return null;
14503
+ const token = text.slice(hash + 1);
14504
+ const path34 = text.slice(0, hash);
14505
+ return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path34.includes("\0") ? { path: path34, token } : null;
14506
+ }
14507
+ function isObject2(value) {
14508
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14509
+ }
14510
+ function parseHandoffToTest(line) {
14511
+ let value;
14512
+ try {
14513
+ value = JSON.parse(line);
14514
+ } catch {
14515
+ return null;
14516
+ }
14517
+ if (!isObject2(value)) return null;
14518
+ switch (value.type) {
14519
+ case "refused":
14520
+ return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
14521
+ case "load":
14522
+ return typeof value.bundle === "string" && value.bundle ? { type: "load", bundle: value.bundle } : null;
14523
+ case "answer":
14524
+ return typeof value.call === "number" ? { type: "answer", call: value.call, value: value.value } : null;
14525
+ case "dispatch":
14526
+ return { type: "dispatch", message: value.message };
14527
+ case "stop":
14528
+ return { type: "stop" };
14529
+ default:
14530
+ return null;
14531
+ }
14532
+ }
14533
+ function lineReader(onLine, onOverflow) {
14534
+ let buffered = "";
14535
+ return (chunk) => {
14536
+ buffered += chunk;
14537
+ for (let end = buffered.indexOf("\n"); end >= 0; end = buffered.indexOf("\n")) {
14538
+ const line = buffered.slice(0, end);
14539
+ buffered = buffered.slice(end + 1);
14540
+ if (line.trim()) onLine(line);
14541
+ }
14542
+ if (buffered.length > MAX_HANDOFF_LINE) {
14543
+ buffered = "";
14544
+ onOverflow();
14545
+ }
14546
+ };
14547
+ }
14548
+
14549
+ // src/internal/capture/record-handoff.ts
14550
+ var START_WAIT_MS = 6e4;
14551
+ var NEW_TAB_NOTICE = "A new tab opened: what you do there is not recorded yet. Record in the first tab.";
14552
+ var relays = /* @__PURE__ */ new WeakMap();
14553
+ function idleAnswer(payload) {
14554
+ const request3 = payload ?? {};
14555
+ if (request3.kind === "local-get") return {};
14556
+ if (request3.kind !== "message") return null;
14557
+ if (request3.message?.type === "piwi-ping") return { ok: true };
14558
+ if (request3.message?.type === "piwi-session-storage") {
14559
+ return request3.message.op === "get" ? { ok: true, items: {} } : { ok: true };
14560
+ }
14561
+ return { ok: false, error: "No recording runs in this page." };
14562
+ }
14563
+ function canRecordFromHere(channel = process.env[RECORD_CHANNEL_ENV]) {
14564
+ return parseRecordChannel(channel) !== null;
14565
+ }
14566
+ function handoffPlace(location, cwd = process.cwd()) {
14567
+ const m = /^(.+):(\d+):\d+$/.exec(location);
14568
+ if (!m) return null;
14569
+ return { file: path29.resolve(cwd, m[1]), line: Number(m[2]) };
14570
+ }
14571
+ async function recordFromHere(options) {
14572
+ const channel = parseRecordChannel(options.channel ?? process.env[RECORD_CHANNEL_ENV]);
14573
+ const place = handoffPlace(options.location);
14574
+ if (!channel || !place) return "this run names no editor to record into.";
14575
+ const { page, testInfo, ended } = options;
14576
+ if (ended?.aborted) return "the test ended.";
14577
+ const use = testInfo.project?.use ?? {};
14578
+ const context = page.context();
14579
+ return new Promise((resolve11) => {
14580
+ const socket = net.connect(channel.path);
14581
+ socket.setEncoding("utf8");
14582
+ const answers = /* @__PURE__ */ new Map();
14583
+ let calls = 0;
14584
+ let attached = false;
14585
+ let recording = false;
14586
+ let settled = false;
14587
+ let initScript;
14588
+ const send = (message) => {
14589
+ if (!socket.destroyed) socket.write(`${JSON.stringify(message)}
14590
+ `);
14591
+ };
14592
+ const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
14593
+ const onGone = () => {
14594
+ if (attached) send({ type: "closed" });
14595
+ finish("the page closed.");
14596
+ };
14597
+ const onEnded = () => {
14598
+ try {
14599
+ if (recording) {
14600
+ void internalCall(
14601
+ page,
14602
+ () => page.evaluate(
14603
+ ([name, value]) => {
14604
+ const dispatch = globalThis[name];
14605
+ if (typeof dispatch === "function") dispatch(value);
14606
+ },
14607
+ [IDE_DISPATCH_GLOBAL, { type: "piwi-recording-stopped" }]
14608
+ )
14609
+ ).catch(() => void 0);
14610
+ }
14611
+ if (attached) send({ type: "closed" });
14612
+ finish("the test ended.");
14613
+ } catch {
14614
+ }
14615
+ };
14616
+ page.once("close", onGone);
14617
+ page.once("crash", onGone);
14618
+ ended?.addEventListener("abort", onEnded, { once: true });
14619
+ const startWait = setTimeout(() => finish("the editor did not start the recording."), START_WAIT_MS);
14620
+ const release = () => {
14621
+ const relay = relays.get(context);
14622
+ if (relay && relay.recording === relayed) relay.recording = null;
14623
+ void Promise.resolve(initScript?.dispose?.()).catch(() => void 0);
14624
+ initScript = void 0;
14625
+ };
14626
+ const finish = (said) => {
14627
+ if (settled) return;
14628
+ settled = true;
14629
+ clearTimeout(startWait);
14630
+ release();
14631
+ for (const [call, answer] of answers) {
14632
+ answers.delete(call);
14633
+ answer(null);
14634
+ }
14635
+ context.off("page", onPage);
14636
+ page.off("close", onGone);
14637
+ page.off("crash", onGone);
14638
+ ended?.removeEventListener("abort", onEnded);
14639
+ socket.end();
14640
+ resolve11(recording ? null : said);
14641
+ };
14642
+ const relayed = (source, payload) => new Promise((answer) => {
14643
+ const call = ++calls;
14644
+ answers.set(call, answer);
14645
+ const outside = source.page !== page || source.frame !== page.mainFrame();
14646
+ send({ type: "binding", call, outside, payload });
14647
+ });
14648
+ const load = async (bundle) => {
14649
+ if (!/^https?:/i.test(page.url())) {
14650
+ const why = `the recorder records http(s) pages only, and this page is ${page.url() || "blank"}.`;
14651
+ send({ type: "notice", message: `Nothing can be recorded here: ${why}` });
14652
+ finish(why);
14653
+ return;
14654
+ }
14655
+ let relay = relays.get(context);
14656
+ if (!relay) {
14657
+ const created = { recording: null, lingering: false };
14658
+ await context.exposeBinding(
14659
+ IDE_RECORDER_BINDING,
14660
+ (source, payload) => created.recording ? created.recording(source, payload) : idleAnswer(payload)
14661
+ );
14662
+ relays.set(context, created);
14663
+ relay = created;
14664
+ }
14665
+ if (settled) return;
14666
+ relay.recording = relayed;
14667
+ if (!relay.lingering) {
14668
+ initScript = await context.addInitScript({ path: bundle });
14669
+ if (typeof initScript?.dispose !== "function") relay.lingering = true;
14670
+ }
14671
+ if (settled) return release();
14672
+ await page.evaluate(fs26.readFileSync(bundle, "utf-8"));
14673
+ if (settled) return release();
14674
+ recording = true;
14675
+ clearTimeout(startWait);
14676
+ context.on("page", onPage);
14677
+ send({ type: "loaded" });
14678
+ };
14679
+ socket.on("connect", () => {
14680
+ attached = true;
14681
+ send({
14682
+ type: "attach",
14683
+ token: channel.token,
14684
+ file: place.file,
14685
+ line: place.line,
14686
+ reason: options.reason,
14687
+ testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
14688
+ baseURL: typeof use.baseURL === "string" ? use.baseURL : null
14689
+ });
14690
+ });
14691
+ socket.on(
14692
+ "data",
14693
+ lineReader(
14694
+ (line) => {
14695
+ const message = parseHandoffToTest(line);
14696
+ if (!message || settled) return;
14697
+ switch (message.type) {
14698
+ case "refused":
14699
+ return finish(message.message);
14700
+ case "load":
14701
+ return void load(message.bundle).catch(
14702
+ (error) => finish(`the recorder could not be loaded: ${error instanceof Error ? error.message : String(error)}`)
14703
+ );
14704
+ case "answer": {
14705
+ const answer = answers.get(message.call);
14706
+ answers.delete(message.call);
14707
+ return answer?.(message.value);
14708
+ }
14709
+ case "dispatch":
14710
+ return void page.evaluate(
14711
+ ([name, value]) => {
14712
+ const dispatch = globalThis[name];
14713
+ if (typeof dispatch === "function") dispatch(value);
14714
+ },
14715
+ [IDE_DISPATCH_GLOBAL, message.message]
14716
+ ).catch(() => void 0);
14717
+ case "stop":
14718
+ return finish(null);
14719
+ }
14720
+ },
14721
+ () => finish("the editor sent a message too long to read.")
14722
+ )
14723
+ );
14724
+ socket.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
14725
+ socket.on("close", () => finish("the editor closed the recording."));
14726
+ });
14727
+ }
14728
+
14132
14729
  // src/internal/capture/page-interaction.ts
14133
14730
  function readPageInteraction() {
14134
14731
  const doc = globalThis.document;
@@ -14424,6 +15021,13 @@ function createSink() {
14424
15021
  pickOffered: false,
14425
15022
  userPick: null,
14426
15023
  pause: null,
15024
+ locatorCalls: /* @__PURE__ */ new Map(),
15025
+ stops: [],
15026
+ shownStops: /* @__PURE__ */ new Map(),
15027
+ released: [],
15028
+ releasedRequests: /* @__PURE__ */ new Set(),
15029
+ onReleasedRequestsEnded: null,
15030
+ releasedQuietSince: 0,
14427
15031
  locatorPages: new LocatorPageLog(),
14428
15032
  codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
14429
15033
  serverCoverage: /* @__PURE__ */ new Map(),
@@ -14449,7 +15053,7 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
14449
15053
  async function startCodeReachOnOpenPages() {
14450
15054
  if (!browserCoverageInWorker() || OPEN_PAGES.size === 0) return;
14451
15055
  const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
14452
- await Promise.race([started, new Promise((resolve9) => setTimeout(resolve9, CODE_REACH_START_WAIT_MS).unref())]);
15056
+ await Promise.race([started, new Promise((resolve11) => setTimeout(resolve11, CODE_REACH_START_WAIT_MS).unref())]);
14453
15057
  }
14454
15058
  function configuredCodeReachRoots() {
14455
15059
  const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
@@ -14463,7 +15067,7 @@ function configuredCodeReachRoots() {
14463
15067
  }
14464
15068
  } catch {
14465
15069
  }
14466
- return raw.split(path28.delimiter).filter(Boolean);
15070
+ return raw.split(path30.delimiter).filter(Boolean);
14467
15071
  }
14468
15072
  async function stopSinkCodeReach(sink, closing) {
14469
15073
  const stopping = [];
@@ -14512,8 +15116,8 @@ async function drainPendingProbes(capMs) {
14512
15116
  let cap;
14513
15117
  await Promise.race([
14514
15118
  Promise.allSettled(PENDING_PROBES),
14515
- new Promise((resolve9) => {
14516
- cap = setTimeout(resolve9, capMs);
15119
+ new Promise((resolve11) => {
15120
+ cap = setTimeout(resolve11, capMs);
14517
15121
  })
14518
15122
  ]);
14519
15123
  clearTimeout(cap);
@@ -14553,10 +15157,10 @@ async function readWebVitals(page) {
14553
15157
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
14554
15158
  paint[key] = Math.round(entry.startTime);
14555
15159
  }
14556
- const readBuffered = (type, extra) => new Promise((resolve9) => {
15160
+ const readBuffered = (type, extra) => new Promise((resolve11) => {
14557
15161
  try {
14558
15162
  const PO = globalThis.PerformanceObserver;
14559
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve9(null);
15163
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve11(null);
14560
15164
  const out = [];
14561
15165
  const po = new PO((list) => out.push(...list.getEntries()));
14562
15166
  po.observe({ type, buffered: true, ...extra });
@@ -14566,10 +15170,10 @@ async function readWebVitals(page) {
14566
15170
  po.disconnect();
14567
15171
  } catch {
14568
15172
  }
14569
- resolve9(out);
15173
+ resolve11(out);
14570
15174
  }, 0);
14571
15175
  } catch {
14572
- resolve9(null);
15176
+ resolve11(null);
14573
15177
  }
14574
15178
  });
14575
15179
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -14684,12 +15288,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
14684
15288
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
14685
15289
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
14686
15290
  function withTimeout(promise, ms) {
14687
- return new Promise((resolve9, reject) => {
15291
+ return new Promise((resolve11, reject) => {
14688
15292
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
14689
15293
  promise.then(
14690
15294
  (value) => {
14691
15295
  clearTimeout(timer);
14692
- resolve9(value);
15296
+ resolve11(value);
14693
15297
  },
14694
15298
  (error) => {
14695
15299
  clearTimeout(timer);
@@ -14804,6 +15408,7 @@ async function maybeOpenPicker(sink, closing) {
14804
15408
  var INSTRUMENTED_PAGES = /* @__PURE__ */ new WeakSet();
14805
15409
  var OPEN_PAGES = /* @__PURE__ */ new Set();
14806
15410
  var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
15411
+ var OPEN_CONTEXTS = /* @__PURE__ */ new Set();
14807
15412
  var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
14808
15413
  var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
14809
15414
  var ACTION_METHOD_SET = new Set(ACTION_METHODS);
@@ -14924,8 +15529,20 @@ function recordLocatorPage(sink, page, locator, location, arrival) {
14924
15529
  } catch {
14925
15530
  }
14926
15531
  }
14927
- function pausesAt(pause, callerLocation) {
14928
- return !pause.finished && (pause.next || pause.set.matches(callerLocation));
15532
+ function knowLocatorCall(sink, page, target, location, origin) {
15533
+ if (!sink?.pause || !location) return null;
15534
+ let settle = () => {
15535
+ };
15536
+ const settled = new Promise((resolve11) => settle = resolve11);
15537
+ const call = { page, target, location, origin, picked: false, settled, settle };
15538
+ sink.locatorCalls.set(callSiteLine(location), call);
15539
+ return call;
15540
+ }
15541
+ function forgetLocatorCall(sink, call) {
15542
+ if (!sink || !call) return;
15543
+ call.settle();
15544
+ const key = callSiteLine(call.location);
15545
+ if (sink.locatorCalls.get(key) === call) sink.locatorCalls.delete(key);
14929
15546
  }
14930
15547
  function locatorSource(target, method, args) {
14931
15548
  try {
@@ -14935,42 +15552,220 @@ function locatorSource(target, method, args) {
14935
15552
  }
14936
15553
  return renderFailing({ method, args });
14937
15554
  }
14938
- async function pauseBefore(sink, page, target, callerLocation, action, origin) {
14939
- const pause = sink.pause;
15555
+ var PAGE_PAUSES = /* @__PURE__ */ new WeakMap();
15556
+ var PLAYWRIGHT_PAUSES = /* @__PURE__ */ new WeakMap();
15557
+ async function armBreakpoints(contexts) {
15558
+ const state = currentSink?.pause;
15559
+ if (!state || state.finished || state.set.size === 0) return;
15560
+ await Promise.all(
15561
+ [...contexts].map(
15562
+ (context) => context && watchStops(context, onDebuggerStop) ? steerDebugger(context, "pause") : void 0
15563
+ )
15564
+ );
15565
+ }
15566
+ function onDebuggerStop(context) {
15567
+ const details = contextDebugger(context)?.pausedDetails();
15568
+ if (!details || PLAYWRIGHT_PAUSES.has(context)) return;
15569
+ const sink = currentSink;
15570
+ const state = sink?.pause ?? null;
15571
+ void (async () => {
15572
+ let how = "resume";
15573
+ try {
15574
+ if (!sink || !state) return;
15575
+ const callSite = stopSite(sink, details.location);
15576
+ const pagePause = PAGE_PAUSES.get(context);
15577
+ const decided = pagePause ? { show: "page-pause" } : atStop(state, callSite);
15578
+ if ("go" in decided) {
15579
+ how = decided.go;
15580
+ return;
15581
+ }
15582
+ how = goOn(state, await showStop(sink, context, details.title, callSite ?? pagePause, decided.show));
15583
+ } catch {
15584
+ if (state) how = goOn(state);
15585
+ } finally {
15586
+ await steerDebugger(context, how);
15587
+ }
15588
+ })();
15589
+ }
15590
+ function stopSite(sink, location) {
15591
+ if (path30.dirname(path30.resolve(location.file)) !== __dirname) return stopCallSite(location);
15592
+ return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
15593
+ }
15594
+ function stopPage(sink, context, call) {
15595
+ for (const page of [call?.page, sink.lastActivePage]) {
15596
+ if (page && !isPageClosed(page) && pageContext(page) === context) return page;
15597
+ }
15598
+ const open = context.pages().filter((page) => !isPageClosed(page));
15599
+ return open[open.length - 1] ?? null;
15600
+ }
15601
+ async function showStop(sink, context, title, location, reason) {
14940
15602
  const testInfo = sink.testInfo;
14941
- if (!pause || !testInfo || !callerLocation) return;
14942
- pause.next = false;
14943
- try {
14944
- const locator = locatorSource(target, origin.method, origin.args);
14945
- const onPick = async (picked) => {
14946
- const pick = {
14947
- failing: { method: origin.method, args: origin.args, rendered: locator, location: callerLocation },
15603
+ const call = sink.locatorCalls.get(callSiteLine(location));
15604
+ const page = stopPage(sink, context, call);
15605
+ if (!testInfo || !page) return void 0;
15606
+ const onPick = async (picked) => {
15607
+ if (call) {
15608
+ call.picked = true;
15609
+ const { method, args } = call.origin;
15610
+ const rendered = locatorSource(call.target, method, args);
15611
+ applyPickToSnapshots(sink.capturedLocators, {
15612
+ failing: { method, args, rendered, location: call.location },
14948
15613
  ...picked
14949
- };
14950
- applyPickToSnapshots(sink.capturedLocators, pick);
14951
- sink.probedLocations.add(callerLocation);
14952
- const place = callerLocation.slice(0, callerLocation.lastIndexOf(":"));
14953
- console.log(`[piwi] Locator picked at ${place}: ${picked.picked.locator}`);
14954
- await postPickToEditor(
14955
- process.env[PIWI_LOCAL_DEBUG_ENV.editorSend],
14956
- pickPayload(picked.picked.locator, callerLocation)
14957
- );
14958
- };
14959
- const choice = await internalCall(
15614
+ });
15615
+ sink.probedLocations.add(call.location);
15616
+ }
15617
+ console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
15618
+ await postPickToEditor(process.env[PIWI_LOCAL_DEBUG_ENV.editorSend], pickPayload(picked.picked.locator, location));
15619
+ };
15620
+ const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
15621
+ sink.stops.push(record);
15622
+ const end = new AbortController();
15623
+ const shown = withTestTimeoutHeld(
15624
+ testInfo,
15625
+ () => internalCall(
14960
15626
  page,
14961
15627
  () => pauseHere(
14962
15628
  page,
14963
- testInfo,
14964
- { location: callerLocation, action, locator, target },
15629
+ {
15630
+ location,
15631
+ call: title,
15632
+ attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
15633
+ target: call?.page === page ? call.target : null
15634
+ },
14965
15635
  { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
14966
- onPick
15636
+ onPick,
15637
+ recorderAt(page, testInfo, location, reason === "page-pause" ? "pause" : "breakpoint"),
15638
+ end.signal
14967
15639
  )
14968
- );
14969
- if (choice === "step") pause.next = true;
14970
- else if (choice === "finish") pause.finished = true;
14971
- } catch {
15640
+ )
15641
+ ).finally(() => {
15642
+ if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
15643
+ else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
15644
+ sink.shownStops.delete(end);
15645
+ });
15646
+ const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
15647
+ sink.shownStops.set(end, { page, record, done: shown.catch(() => void 0), heldAt });
15648
+ const answer = await shown;
15649
+ if (!end.signal.aborted) return answer;
15650
+ return end.signal.reason === "closed" ? "resume" : "finish";
15651
+ }
15652
+ var END_STOP_MS = 2e3;
15653
+ var RELEASED_CALL_MS = 2e3;
15654
+ var RELEASED_QUIET_MS = 200;
15655
+ async function endShownStops(sink, why, closing) {
15656
+ const ending = [];
15657
+ let heldAt = Infinity;
15658
+ for (const [end, { page, record, done, heldAt: stopHeldAt }] of sink.shownStops) {
15659
+ if (closing && closing.page !== page && (!closing.context || pageContext(page) !== closing.context)) continue;
15660
+ if (!end.signal.aborted) {
15661
+ record.duration = Date.now() - record.startedAt;
15662
+ end.abort(why);
15663
+ if (why === "ended" && stopHeldAt !== null) heldAt = Math.min(heldAt, stopHeldAt);
15664
+ }
15665
+ ending.push(done);
15666
+ }
15667
+ if (sink.testInfo && heldAt !== Infinity) restoreTestTime(sink.testInfo, heldAt);
15668
+ await settledWithin(ending, END_STOP_MS);
15669
+ }
15670
+ function releaseCall(sink, settled) {
15671
+ sink.releasedQuietSince = Date.now();
15672
+ sink.released.push(settled.then(() => void (sink.releasedQuietSince = Date.now())));
15673
+ }
15674
+ function releasedRequestsEnded(sink) {
15675
+ if (sink.releasedRequests.size === 0) return Promise.resolve();
15676
+ return new Promise((resolve11) => sink.onReleasedRequestsEnded = resolve11);
15677
+ }
15678
+ async function awaitReleasedCalls(sink) {
15679
+ if (sink.released.length === 0) return;
15680
+ const deadline = Date.now() + RELEASED_CALL_MS;
15681
+ await settledWithin(sink.released, RELEASED_CALL_MS);
15682
+ for (let now = Date.now(); now < deadline; now = Date.now()) {
15683
+ if (sink.releasedRequests.size > 0) {
15684
+ await settledWithin([releasedRequestsEnded(sink)], deadline - now);
15685
+ continue;
15686
+ }
15687
+ const quiet = now - sink.releasedQuietSince;
15688
+ if (quiet >= RELEASED_QUIET_MS) return;
15689
+ await new Promise((resolve11) => setTimeout(resolve11, Math.min(RELEASED_QUIET_MS - quiet, deadline - now)));
14972
15690
  }
14973
15691
  }
15692
+ async function settledWithin(pending, ms) {
15693
+ if (pending.length === 0) return;
15694
+ let timer;
15695
+ await Promise.race([Promise.allSettled(pending), new Promise((resolve11) => timer = setTimeout(resolve11, ms))]);
15696
+ clearTimeout(timer);
15697
+ }
15698
+ function endStopsWithTestFunction(testInfo, sink) {
15699
+ const callbacks = testInfo._onDidFinishTestFunctionCallbacks;
15700
+ if (!(callbacks instanceof Set)) return;
15701
+ const theirs = [...callbacks];
15702
+ callbacks.clear();
15703
+ callbacks.add(() => endShownStops(sink, "ended"));
15704
+ for (const callback of theirs) callbacks.add(callback);
15705
+ }
15706
+ function recorderAt(page, testInfo, location, reason) {
15707
+ if (!canRecordFromHere()) return void 0;
15708
+ return async (ended) => {
15709
+ console.log(`[piwi] Recording from ${callSiteLine(location)}: the editor writes the steps.`);
15710
+ const why = await recordFromHere({ page, testInfo, location, reason, ended });
15711
+ if (ended?.aborted) console.log("[piwi] The recording ended with the test.");
15712
+ else
15713
+ console.log(
15714
+ why ? `[piwi] Nothing was recorded: ${why}` : "[piwi] The recording ended: the test is paused again."
15715
+ );
15716
+ };
15717
+ }
15718
+ async function pauseInPlace(page, playwrightPause, location) {
15719
+ const sink = currentSink;
15720
+ const testInfo = sink?.testInfo;
15721
+ const context = pageContext(page);
15722
+ if (!sink || !testInfo || !location || !context || !contextDebugger(context)) {
15723
+ return leaveToPlaywright(context, playwrightPause);
15724
+ }
15725
+ if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
15726
+ return leaveToPlaywright(context, playwrightPause);
15727
+ }
15728
+ sink.pause ??= { set: NO_BREAKPOINTS, step: false, finished: false };
15729
+ watchStops(context, onDebuggerStop);
15730
+ await steerDebugger(context, "pause");
15731
+ PAGE_PAUSES.set(context, location);
15732
+ try {
15733
+ await playwrightPause({ __testHookKeepTestTimeout: true });
15734
+ } finally {
15735
+ PAGE_PAUSES.delete(context);
15736
+ }
15737
+ }
15738
+ async function leaveToPlaywright(context, playwrightPause) {
15739
+ if (context) PLAYWRIGHT_PAUSES.set(context, (PLAYWRIGHT_PAUSES.get(context) ?? 0) + 1);
15740
+ try {
15741
+ await withInspector(() => playwrightPause());
15742
+ } finally {
15743
+ if (context) {
15744
+ const left = (PLAYWRIGHT_PAUSES.get(context) ?? 1) - 1;
15745
+ if (left > 0) PLAYWRIGHT_PAUSES.set(context, left);
15746
+ else PLAYWRIGHT_PAUSES.delete(context);
15747
+ }
15748
+ }
15749
+ }
15750
+ var PLAYWRIGHT_PAUSE = /* @__PURE__ */ Symbol.for("@piwitests/reporter:playwright-pause");
15751
+ function playwrightPauseOf(page) {
15752
+ const current = page.pause;
15753
+ if (typeof current !== "function") return null;
15754
+ const own = current[PLAYWRIGHT_PAUSE] ?? current;
15755
+ const proto = Object.getPrototypeOf(page);
15756
+ const theirs = proto?.pause;
15757
+ if (proto && typeof theirs === "function") {
15758
+ if (!theirs[PLAYWRIGHT_PAUSE]) {
15759
+ const wrapped = function(options) {
15760
+ return leaveToPlaywright(pageContext(this), () => theirs.call(this, options));
15761
+ };
15762
+ wrapped[PLAYWRIGHT_PAUSE] = theirs;
15763
+ proto.pause = wrapped;
15764
+ }
15765
+ markPausesWrapped();
15766
+ }
15767
+ return (options) => own.call(page, options);
15768
+ }
14974
15769
  function wrapLocator(page, locator, originMethod, originArgs) {
14975
15770
  return new Proxy(locator, {
14976
15771
  get(target, prop) {
@@ -14992,36 +15787,35 @@ function wrapLocator(page, locator, originMethod, originArgs) {
14992
15787
  const isNot = Boolean(callArgs[1]?.isNot);
14993
15788
  const callerLocation = sink ? captureCallerLocation() : null;
14994
15789
  if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
14995
- if (sink?.pause && pausesAt(sink.pause, callerLocation)) {
14996
- await pauseBefore(sink, page, target, callerLocation, assertionLabel(expression, isNot), {
15790
+ const call = knowLocatorCall(sink, page, target, callerLocation, { method: originMethod, args: originArgs });
15791
+ try {
15792
+ if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
15793
+ return await callMethod(target, prop, callArgs);
15794
+ }
15795
+ sink.lastActivePage = page;
15796
+ const used = {
14997
15797
  method: originMethod,
14998
- args: originArgs
14999
- });
15000
- }
15001
- if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
15002
- return callMethod(target, prop, callArgs);
15003
- }
15004
- sink.lastActivePage = page;
15005
- const used = {
15006
- method: originMethod,
15007
- args: originArgs,
15008
- raw: `${originMethod}(${JSON.stringify(originArgs)})`
15009
- };
15010
- const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
15011
- let seq = -1;
15012
- if (!alreadyProbed) {
15013
- seq = sink.capturedLocators.length;
15014
- sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
15015
- }
15016
- const result = await callMethod(target, prop, callArgs);
15017
- const matches = result?.matches;
15018
- if (matches === true && !alreadyProbed) {
15019
- if (callerLocation) sink.probedLocations.add(callerLocation);
15020
- startElementCapture(sink, page, target, seq, callerLocation, used);
15021
- } else if (matches === false) {
15022
- sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
15798
+ args: originArgs,
15799
+ raw: `${originMethod}(${JSON.stringify(originArgs)})`
15800
+ };
15801
+ const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
15802
+ let seq = -1;
15803
+ if (!alreadyProbed) {
15804
+ seq = sink.capturedLocators.length;
15805
+ sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
15806
+ }
15807
+ const result = await callMethod(target, prop, callArgs);
15808
+ const matches = result?.matches;
15809
+ if (matches === true && !alreadyProbed && !call?.picked) {
15810
+ if (callerLocation) sink.probedLocations.add(callerLocation);
15811
+ startElementCapture(sink, page, target, seq, callerLocation, used);
15812
+ } else if (matches === false) {
15813
+ sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
15814
+ }
15815
+ return result;
15816
+ } finally {
15817
+ forgetLocatorCall(sink, call);
15023
15818
  }
15024
- return result;
15025
15819
  };
15026
15820
  }
15027
15821
  if (!ACTION_METHOD_SET.has(prop)) return original;
@@ -15036,9 +15830,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
15036
15830
  args: originArgs,
15037
15831
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
15038
15832
  };
15039
- if (sink.pause && pausesAt(sink.pause, callerLocation)) {
15040
- await pauseBefore(sink, page, target, callerLocation, String(prop), used);
15041
- }
15833
+ const call = knowLocatorCall(sink, page, target, callerLocation, used);
15042
15834
  const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
15043
15835
  let seq = -1;
15044
15836
  if (!alreadyProbed) {
@@ -15056,8 +15848,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
15056
15848
  } catch (error) {
15057
15849
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
15058
15850
  throw error;
15851
+ } finally {
15852
+ forgetLocatorCall(sink, call);
15059
15853
  }
15060
- if (!alreadyProbed) {
15854
+ if (!alreadyProbed && !call?.picked) {
15061
15855
  if (callerLocation) sink.probedLocations.add(callerLocation);
15062
15856
  startElementCapture(sink, page, target, seq, callerLocation, used);
15063
15857
  }
@@ -15110,12 +15904,17 @@ function instrumentPage(page) {
15110
15904
  }
15111
15905
  const ctx = pageContext(page);
15112
15906
  if (ctx) instrumentContext(ctx);
15907
+ const playwrightPause = playwrightPauseOf(page);
15908
+ if (playwrightPause) {
15909
+ page.pause = () => pauseInPlace(page, playwrightPause, captureCallerLocation());
15910
+ }
15113
15911
  if (typeof page.close === "function") {
15114
15912
  const originalClose = page.close.bind(page);
15115
15913
  page.close = async (...args) => {
15116
15914
  await drainPendingProbes(1e3);
15117
15915
  const sink = currentSink;
15118
15916
  if (sink) {
15917
+ await endShownStops(sink, "closed", { page });
15119
15918
  await stashPageState(sink, { page });
15120
15919
  await maybeOpenPicker(sink, { page });
15121
15920
  await stopSinkCodeReach(sink, { page });
@@ -15204,8 +16003,18 @@ function instrumentPage(page) {
15204
16003
  } catch {
15205
16004
  }
15206
16005
  });
16006
+ page.on("request", (request3) => {
16007
+ const sink = currentSink;
16008
+ if (!sink || sink.released.length === 0) return;
16009
+ sink.releasedRequests.add(request3);
16010
+ sink.releasedQuietSince = Date.now();
16011
+ });
15207
16012
  const onRequestEnd = (request3, failed) => {
15208
16013
  const sink = currentSink;
16014
+ if (sink?.releasedRequests.delete(request3)) {
16015
+ sink.releasedQuietSince = Date.now();
16016
+ if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
16017
+ }
15209
16018
  if (!sink || RECORDED_OPEN.has(request3)) return;
15210
16019
  sink.lastActivePage = page;
15211
16020
  const response = sink.openResponses.get(request3) ?? null;
@@ -15311,6 +16120,10 @@ async function awaitFlakeConditions() {
15311
16120
  function instrumentContext(context) {
15312
16121
  if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
15313
16122
  INSTRUMENTED_CONTEXTS.add(context);
16123
+ if (typeof context.once === "function") {
16124
+ OPEN_CONTEXTS.add(context);
16125
+ context.once("close", () => OPEN_CONTEXTS.delete(context));
16126
+ }
15314
16127
  observeOwnApiRequests(context);
15315
16128
  if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
15316
16129
  void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
@@ -15330,6 +16143,7 @@ function instrumentContext(context) {
15330
16143
  await drainPendingProbes(1e3);
15331
16144
  const sink = currentSink;
15332
16145
  if (sink) {
16146
+ await endShownStops(sink, "closed", { context });
15333
16147
  await stashPageState(sink, { context });
15334
16148
  await maybeOpenPicker(sink, { context });
15335
16149
  await stopSinkCodeReach(sink, { context });
@@ -15346,6 +16160,7 @@ function patchBrowser(browser) {
15346
16160
  browser.newPage = async (...args) => {
15347
16161
  const page = await originalNewPage(...args);
15348
16162
  instrumentPage(page);
16163
+ await armBreakpoints([pageContext(page)]);
15349
16164
  await awaitFlakeConditions();
15350
16165
  await maybeStartCodeReach(page);
15351
16166
  return page;
@@ -15354,6 +16169,7 @@ function patchBrowser(browser) {
15354
16169
  browser.newContext = async (...args) => {
15355
16170
  const context = await originalNewContext(...args);
15356
16171
  instrumentContext(context);
16172
+ await armBreakpoints([context]);
15357
16173
  return context;
15358
16174
  };
15359
16175
  if (typeof browser.close === "function") {
@@ -15405,14 +16221,20 @@ async function flushSink(sink, testInfo) {
15405
16221
  let drainDeadline;
15406
16222
  await Promise.race([
15407
16223
  Promise.allSettled(sink.capturePromises),
15408
- new Promise((resolve9) => {
15409
- drainDeadline = setTimeout(resolve9, 2e3);
16224
+ new Promise((resolve11) => {
16225
+ drainDeadline = setTimeout(resolve11, 2e3);
15410
16226
  })
15411
16227
  ]);
15412
16228
  clearTimeout(drainDeadline);
15413
16229
  const page = sink.lastActivePage;
15414
16230
  const pageReadable = page !== null && !isPageClosed(page);
15415
16231
  if (pageReadable) await maybeOpenPicker(sink);
16232
+ if (sink.stops.length > 0) {
16233
+ await testInfo.attach(ATTACHMENT_NAMES.pauses, {
16234
+ contentType: "application/json",
16235
+ body: Buffer.from(JSON.stringify(sink.stops))
16236
+ });
16237
+ }
15416
16238
  if (sink.capturedLocators.length > 0) {
15417
16239
  await testInfo.attach(ATTACHMENT_NAMES.locators, {
15418
16240
  contentType: "application/json",
@@ -15613,6 +16435,7 @@ var piwiFixtures = {
15613
16435
  // never double-wraps a page the browser patch already instrumented.
15614
16436
  page: async ({ page }, use) => {
15615
16437
  instrumentPage(page);
16438
+ await armBreakpoints([pageContext(page)]);
15616
16439
  await awaitFlakeConditions();
15617
16440
  await maybeStartCodeReach(page);
15618
16441
  await use(page);
@@ -15633,8 +16456,9 @@ var piwiFixtures = {
15633
16456
  if (pauseSet.size) {
15634
16457
  const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
15635
16458
  if (reason) logPauseSkipOnce(reason);
15636
- else sink.pause = { set: pauseSet, next: false, finished: false };
16459
+ else sink.pause = { set: pauseSet, step: false, finished: false };
15637
16460
  }
16461
+ if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
15638
16462
  if (isFlakeMode()) {
15639
16463
  const plan = loadFlakePlan();
15640
16464
  const role = flakeRoleForTest(plan, {
@@ -15659,9 +16483,12 @@ var piwiFixtures = {
15659
16483
  activeResourceLedger()?.testStarted(testInfo);
15660
16484
  currentSink = sink;
15661
16485
  try {
16486
+ await armBreakpoints(OPEN_CONTEXTS);
15662
16487
  await startCodeReachOnOpenPages();
15663
16488
  await use();
15664
16489
  } finally {
16490
+ await endShownStops(sink, "ended");
16491
+ await awaitReleasedCalls(sink);
15665
16492
  currentSink = null;
15666
16493
  if (sink.probeItem) {
15667
16494
  const applied = sink.probeInterception?.applied() ?? false;
@@ -15719,13 +16546,13 @@ function extendPiwiFixtures(test) {
15719
16546
  }
15720
16547
 
15721
16548
  // src/internal/ai/ai-fixtures.ts
15722
- var fs26 = __toESM(require("fs"));
15723
- var path31 = __toESM(require("path"));
16549
+ var fs28 = __toESM(require("fs"));
16550
+ var path33 = __toESM(require("path"));
15724
16551
  var import_test = require("@playwright/test");
15725
16552
 
15726
16553
  // src/internal/ai/artifact.ts
15727
- var fs25 = __toESM(require("fs"));
15728
- var path29 = __toESM(require("path"));
16554
+ var fs27 = __toESM(require("fs"));
16555
+ var path31 = __toESM(require("path"));
15729
16556
  var ARTIFACT_VERSION = 1;
15730
16557
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
15731
16558
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -15818,7 +16645,7 @@ function parseEntry(text) {
15818
16645
  function readEntry(file) {
15819
16646
  let text;
15820
16647
  try {
15821
- text = fs25.readFileSync(file, "utf8");
16648
+ text = fs27.readFileSync(file, "utf8");
15822
16649
  } catch (error) {
15823
16650
  if (error.code === "ENOENT") return null;
15824
16651
  throw error;
@@ -15828,28 +16655,28 @@ function readEntry(file) {
15828
16655
  function writeEntry(file, entry) {
15829
16656
  const canonical = serializeEntry(entry);
15830
16657
  return withEntryLock(file, () => {
15831
- if (fs25.existsSync(file) && fs25.readFileSync(file, "utf8") === canonical) return { written: false };
15832
- fs25.mkdirSync(path29.dirname(file), { recursive: true });
16658
+ if (fs27.existsSync(file) && fs27.readFileSync(file, "utf8") === canonical) return { written: false };
16659
+ fs27.mkdirSync(path31.dirname(file), { recursive: true });
15833
16660
  const tmp = `${file}.${process.pid}.tmp`;
15834
- fs25.writeFileSync(tmp, canonical);
15835
- fs25.renameSync(tmp, file);
16661
+ fs27.writeFileSync(tmp, canonical);
16662
+ fs27.renameSync(tmp, file);
15836
16663
  return { written: true };
15837
16664
  });
15838
16665
  }
15839
16666
  function withEntryLock(file, fn) {
15840
16667
  const lock = `${file}.lock`;
15841
- fs25.mkdirSync(path29.dirname(file), { recursive: true });
16668
+ fs27.mkdirSync(path31.dirname(file), { recursive: true });
15842
16669
  const deadline = Date.now() + 5e3;
15843
16670
  for (; ; ) {
15844
16671
  try {
15845
- const fd = fs25.openSync(lock, "wx");
15846
- fs25.closeSync(fd);
16672
+ const fd = fs27.openSync(lock, "wx");
16673
+ fs27.closeSync(fd);
15847
16674
  break;
15848
16675
  } catch (error) {
15849
16676
  if (error.code !== "EEXIST") throw error;
15850
16677
  if (Date.now() > deadline) {
15851
16678
  try {
15852
- fs25.unlinkSync(lock);
16679
+ fs27.unlinkSync(lock);
15853
16680
  } catch {
15854
16681
  }
15855
16682
  }
@@ -15859,7 +16686,7 @@ function withEntryLock(file, fn) {
15859
16686
  return fn();
15860
16687
  } finally {
15861
16688
  try {
15862
- fs25.unlinkSync(lock);
16689
+ fs27.unlinkSync(lock);
15863
16690
  } catch {
15864
16691
  }
15865
16692
  }
@@ -15867,7 +16694,7 @@ function withEntryLock(file, fn) {
15867
16694
 
15868
16695
  // src/internal/ai/keys.ts
15869
16696
  var crypto7 = __toESM(require("crypto"));
15870
- var path30 = __toESM(require("path"));
16697
+ var path32 = __toESM(require("path"));
15871
16698
  var DEFAULT_AI_DIR = "__piwi__";
15872
16699
  function normalizeTemplate(template) {
15873
16700
  return template.trim().replace(/\s+/g, " ").toLowerCase();
@@ -15881,13 +16708,13 @@ function slug(text, maxLength = 40) {
15881
16708
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
15882
16709
  }
15883
16710
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
15884
- return path30.join(path30.dirname(specFile), dir, path30.basename(specFile));
16711
+ return path32.join(path32.dirname(specFile), dir, path32.basename(specFile));
15885
16712
  }
15886
16713
  function entryPath(params) {
15887
16714
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
15888
16715
  const hash = hashTemplate(basis, params.ordinal ?? 0);
15889
16716
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
15890
- return path30.join(entryDir(params.specFile, params.dir), name);
16717
+ return path32.join(entryDir(params.specFile, params.dir), name);
15891
16718
  }
15892
16719
  function parseLocation2(location) {
15893
16720
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -16490,7 +17317,7 @@ function testIdentity(testInfo) {
16490
17317
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
16491
17318
  }
16492
17319
  function relativeToCwd(file) {
16493
- return path31.relative(process.cwd(), file).split(path31.sep).join("/");
17320
+ return path33.relative(process.cwd(), file).split(path33.sep).join("/");
16494
17321
  }
16495
17322
  function recordIntents(intents, entry) {
16496
17323
  const add = (locator, kind) => {
@@ -16517,7 +17344,7 @@ function createAiApi(page, testInfo, config, used, intents) {
16517
17344
  const readSource = () => {
16518
17345
  if (source === null) {
16519
17346
  try {
16520
- source = fs26.readFileSync(testInfo.file, "utf8");
17347
+ source = fs28.readFileSync(testInfo.file, "utf8");
16521
17348
  } catch {
16522
17349
  source = "";
16523
17350
  }