@piwitests/reporter 0.59.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
@@ -858,8 +972,8 @@ var Uploader = class {
858
972
  if (traceInfo) {
859
973
  form.append("trace_hash", traceInfo.hash);
860
974
  if (includeTraceFile2) {
861
- form.append("trace", fs2.createReadStream(traceInfo.tracePath), {
862
- filename: path2.basename(traceInfo.tracePath)
975
+ form.append("trace", fs3.createReadStream(traceInfo.tracePath), {
976
+ filename: path3.basename(traceInfo.tracePath)
863
977
  });
864
978
  }
865
979
  }
@@ -871,7 +985,7 @@ var Uploader = class {
871
985
  )
872
986
  );
873
987
  for (const a of attachments) {
874
- form.append("attach_file", fs2.createReadStream(a.path), { filename: a.originalName });
988
+ form.append("attach_file", fs3.createReadStream(a.path), { filename: a.originalName });
875
989
  }
876
990
  }
877
991
  return form;
@@ -924,7 +1038,7 @@ var Uploader = class {
924
1038
  )
925
1039
  );
926
1040
  for (const a of attachments) {
927
- 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 });
928
1042
  attachmentCount++;
929
1043
  }
930
1044
  }
@@ -933,8 +1047,8 @@ var Uploader = class {
933
1047
  let traceCount = 0;
934
1048
  for (const [i, tc] of testCases.entries()) {
935
1049
  for (const tp of this.fileHandler.findTraceFiles(tc)) {
936
- if (fs2.existsSync(tp)) {
937
- 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) });
938
1052
  traceCount++;
939
1053
  }
940
1054
  }
@@ -944,9 +1058,9 @@ var Uploader = class {
944
1058
  };
945
1059
 
946
1060
  // src/internal/streaming/stream-buffer.ts
947
- var path3 = __toESM(require("path"));
1061
+ var path4 = __toESM(require("path"));
948
1062
  var os3 = __toESM(require("os"));
949
- var fs3 = __toESM(require("fs"));
1063
+ var fs4 = __toESM(require("fs"));
950
1064
 
951
1065
  // src/internal/support/instance-id.ts
952
1066
  var crypto = __toESM(require("crypto"));
@@ -967,14 +1081,14 @@ var StreamBuffer = class {
967
1081
  }
968
1082
  /** Scope the buffer to a run. Until then the buffer is empty and appends are dropped. */
969
1083
  bindRun(runId) {
970
- this.filePath = path3.join(os3.tmpdir(), `${this.prefix}-${runId}.jsonl`);
1084
+ this.filePath = path4.join(os3.tmpdir(), `${this.prefix}-${runId}.jsonl`);
971
1085
  }
972
1086
  /** Append one or more events to the on-disk buffer */
973
1087
  append(events) {
974
1088
  if (!this.filePath || events.length === 0) return;
975
1089
  try {
976
1090
  const lines = events.map((e) => JSON.stringify(e) + "\n").join("");
977
- fs3.appendFileSync(this.filePath, lines, "utf8");
1091
+ fs4.appendFileSync(this.filePath, lines, "utf8");
978
1092
  } catch {
979
1093
  }
980
1094
  }
@@ -982,8 +1096,8 @@ var StreamBuffer = class {
982
1096
  load() {
983
1097
  if (!this.filePath) return [];
984
1098
  try {
985
- if (fs3.existsSync(this.filePath)) {
986
- const content = fs3.readFileSync(this.filePath, "utf8");
1099
+ if (fs4.existsSync(this.filePath)) {
1100
+ const content = fs4.readFileSync(this.filePath, "utf8");
987
1101
  return content.split("\n").filter(Boolean).map((line) => JSON.parse(line));
988
1102
  }
989
1103
  } catch {
@@ -994,7 +1108,7 @@ var StreamBuffer = class {
994
1108
  clear() {
995
1109
  if (!this.filePath) return;
996
1110
  try {
997
- if (fs3.existsSync(this.filePath)) fs3.unlinkSync(this.filePath);
1111
+ if (fs4.existsSync(this.filePath)) fs4.unlinkSync(this.filePath);
998
1112
  } catch {
999
1113
  }
1000
1114
  }
@@ -1003,14 +1117,14 @@ var StreamBuffer = class {
1003
1117
  const dir = os3.tmpdir();
1004
1118
  let names;
1005
1119
  try {
1006
- names = fs3.readdirSync(dir).filter((name) => name.startsWith(this.prefix));
1120
+ names = fs4.readdirSync(dir).filter((name) => name.startsWith(this.prefix));
1007
1121
  } catch {
1008
1122
  return;
1009
1123
  }
1010
1124
  for (const name of names) {
1011
1125
  try {
1012
- const file = path3.join(dir, name);
1013
- 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);
1014
1128
  } catch {
1015
1129
  }
1016
1130
  }
@@ -1019,9 +1133,9 @@ var StreamBuffer = class {
1019
1133
 
1020
1134
  // src/internal/streaming/crash-recovery.ts
1021
1135
  var crypto2 = __toESM(require("crypto"));
1022
- var fs4 = __toESM(require("fs"));
1136
+ var fs5 = __toESM(require("fs"));
1023
1137
  var os4 = __toESM(require("os"));
1024
- var path4 = __toESM(require("path"));
1138
+ var path5 = __toESM(require("path"));
1025
1139
 
1026
1140
  // src/internal/support/errors.ts
1027
1141
  function errorMessage(error) {
@@ -1036,7 +1150,7 @@ function recoveryServer(serverUrl) {
1036
1150
  function recoveryFilePath(serverUrl, projectName) {
1037
1151
  const key = crypto2.createHash("sha256").update(`${recoveryServer(serverUrl)}
1038
1152
  ${projectName}`).digest("hex").slice(0, 16);
1039
- return path4.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
1153
+ return path5.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
1040
1154
  }
1041
1155
  function refusesRun(error) {
1042
1156
  return error instanceof HttpError && [400, 409, 422].includes(error.status);
@@ -1057,11 +1171,11 @@ var CrashRecovery = class {
1057
1171
  const file = { v: 1, serverUrl: this.serverUrl, payload };
1058
1172
  const temp = `${this.filePath}.${process.pid}.${crypto2.randomBytes(6).toString("hex")}.tmp`;
1059
1173
  try {
1060
- fs4.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
1061
- fs4.renameSync(temp, this.filePath);
1174
+ fs5.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
1175
+ fs5.renameSync(temp, this.filePath);
1062
1176
  this.logger.info("Saved recovery data for later upload");
1063
1177
  } catch (error) {
1064
- fs4.rmSync(temp, { force: true });
1178
+ fs5.rmSync(temp, { force: true });
1065
1179
  this.logger.error(`Failed to save recovery data: ${errorMessage(error)}`);
1066
1180
  }
1067
1181
  }
@@ -1072,26 +1186,26 @@ var CrashRecovery = class {
1072
1186
  load() {
1073
1187
  let fd;
1074
1188
  try {
1075
- 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));
1076
1190
  } catch {
1077
1191
  return null;
1078
1192
  }
1079
1193
  try {
1080
- const stat = fs4.fstatSync(fd);
1194
+ const stat = fs5.fstatSync(fd);
1081
1195
  if (!stat.isFile() || typeof process.getuid === "function" && stat.uid !== process.getuid()) return null;
1082
- const file = JSON.parse(fs4.readFileSync(fd, "utf8"));
1196
+ const file = JSON.parse(fs5.readFileSync(fd, "utf8"));
1083
1197
  if (file?.v !== 1 || file.serverUrl !== this.serverUrl) return null;
1084
1198
  return file.payload && typeof file.payload === "object" ? file.payload : null;
1085
1199
  } catch {
1086
1200
  return null;
1087
1201
  } finally {
1088
- fs4.closeSync(fd);
1202
+ fs5.closeSync(fd);
1089
1203
  }
1090
1204
  }
1091
1205
  /** Delete the recovery file from disk */
1092
1206
  clear() {
1093
1207
  try {
1094
- fs4.rmSync(this.filePath, { force: true });
1208
+ fs5.rmSync(this.filePath, { force: true });
1095
1209
  } catch {
1096
1210
  }
1097
1211
  }
@@ -1123,25 +1237,25 @@ var CrashRecovery = class {
1123
1237
  };
1124
1238
 
1125
1239
  // src/internal/files/file-handler.ts
1126
- var fs6 = __toESM(require("fs"));
1240
+ var fs7 = __toESM(require("fs"));
1127
1241
  var os5 = __toESM(require("os"));
1128
- var path6 = __toESM(require("path"));
1242
+ var path7 = __toESM(require("path"));
1129
1243
  var crypto3 = __toESM(require("crypto"));
1130
1244
 
1131
1245
  // src/internal/files/compression.ts
1132
- var fs5 = __toESM(require("fs"));
1133
- var path5 = __toESM(require("path"));
1246
+ var fs6 = __toESM(require("fs"));
1247
+ var path6 = __toESM(require("path"));
1134
1248
  var zlib = __toESM(require("zlib"));
1135
1249
  var import_node_util = require("util");
1136
1250
  var gzipAsync = (0, import_node_util.promisify)(zlib.gzip);
1137
1251
  async function compressDirectory(sourceDir) {
1138
1252
  const files = [];
1139
1253
  function collect(dir, baseDir = "") {
1140
- for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
1141
- const full = path5.join(dir, entry.name);
1142
- 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);
1143
1257
  if (entry.isDirectory()) collect(full, rel);
1144
- 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) });
1145
1259
  }
1146
1260
  }
1147
1261
  collect(sourceDir);
@@ -1205,7 +1319,8 @@ var ATTACHMENT_NAMES = {
1205
1319
  userPick: "piwi-user-pick",
1206
1320
  aiUsage: "piwi-ai-usage",
1207
1321
  aiMeta: "piwi-ai-meta",
1208
- resources: "piwi-resources"
1322
+ resources: "piwi-resources",
1323
+ pauses: "piwi-pauses"
1209
1324
  };
1210
1325
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
1211
1326
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
@@ -1239,19 +1354,19 @@ var FileHandler = class {
1239
1354
  }
1240
1355
  /** Locate a Playwright HTML report directory containing `index.html`. Optionally override the search path. */
1241
1356
  findHTMLReportDirectory(customDir) {
1242
- 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")];
1243
1358
  for (const reportDir of possibleDirs) {
1244
- if (fs6.existsSync(reportDir) && fs6.statSync(reportDir).isDirectory()) {
1245
- 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;
1246
1361
  }
1247
1362
  }
1248
1363
  return null;
1249
1364
  }
1250
1365
  /** Check if `dir` exists and is a directory; also checks `path.join(process.cwd(), dir)` */
1251
1366
  findReportDirectory(dir) {
1252
- const candidates = [dir, path6.join(process.cwd(), dir)];
1367
+ const candidates = [dir, path7.join(process.cwd(), dir)];
1253
1368
  for (const candidate of candidates) {
1254
- if (fs6.existsSync(candidate) && fs6.statSync(candidate).isDirectory()) return candidate;
1369
+ if (fs7.existsSync(candidate) && fs7.statSync(candidate).isDirectory()) return candidate;
1255
1370
  }
1256
1371
  return null;
1257
1372
  }
@@ -1269,7 +1384,7 @@ var FileHandler = class {
1269
1384
  const set = /* @__PURE__ */ new Set();
1270
1385
  if (testCase.attachments) {
1271
1386
  for (const a of testCase.attachments) {
1272
- 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));
1273
1388
  }
1274
1389
  }
1275
1390
  return Array.from(set);
@@ -1297,24 +1412,24 @@ var FileHandler = class {
1297
1412
  cleanupBodyAttachments() {
1298
1413
  if (!this.bodyDir) return;
1299
1414
  try {
1300
- fs6.rmSync(this.bodyDir, { recursive: true, force: true });
1415
+ fs7.rmSync(this.bodyDir, { recursive: true, force: true });
1301
1416
  } catch (error) {
1302
1417
  this.logger.debug(`Could not remove temp attachments at ${this.bodyDir}: ${errorMessage(error)}`);
1303
1418
  }
1304
1419
  this.bodyDir = null;
1305
1420
  }
1306
1421
  resolvePathAttachment(a, testCase) {
1307
- if (!a.path || !fs6.existsSync(a.path)) return null;
1308
- 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;
1309
1424
  if (size > this.maxAttachmentBytes) {
1310
1425
  this.warnOversized(a, testCase, size);
1311
1426
  return null;
1312
1427
  }
1313
1428
  return {
1314
1429
  name: a.name || "attachment",
1315
- path: path6.resolve(a.path),
1430
+ path: path7.resolve(a.path),
1316
1431
  contentType: a.contentType || "application/octet-stream",
1317
- originalName: path6.basename(a.path)
1432
+ originalName: path7.basename(a.path)
1318
1433
  };
1319
1434
  }
1320
1435
  resolveBodyAttachment(a, testCase) {
@@ -1329,8 +1444,8 @@ var FileHandler = class {
1329
1444
  let filePath2 = this.bodyFiles.get(a);
1330
1445
  if (!filePath2) {
1331
1446
  try {
1332
- filePath2 = path6.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
1333
- fs6.writeFileSync(filePath2, body);
1447
+ filePath2 = path7.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
1448
+ fs7.writeFileSync(filePath2, body);
1334
1449
  this.bodyFiles.set(a, filePath2);
1335
1450
  } catch (error) {
1336
1451
  this.logger.warn(`Could not stage attachment "${a.name}" for upload: ${errorMessage(error)}`);
@@ -1340,7 +1455,7 @@ var FileHandler = class {
1340
1455
  return { name: a.name || "attachment", path: filePath2, contentType, originalName };
1341
1456
  }
1342
1457
  bodyAttachmentDir() {
1343
- 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-"));
1344
1459
  return this.bodyDir;
1345
1460
  }
1346
1461
  warnOversized(a, testCase, size) {
@@ -1363,10 +1478,10 @@ var FileHandler = class {
1363
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 */
1364
1479
  parsePerformanceAttachments(testCase, attachments) {
1365
1480
  const find = (name) => attachments.find((a) => a.name === name);
1366
- const net = find(ATTACHMENT_NAMES.network);
1367
- if (net?.body) {
1481
+ const net2 = find(ATTACHMENT_NAMES.network);
1482
+ if (net2?.body) {
1368
1483
  try {
1369
- testCase.networkRequests = JSON.parse(net.body.toString());
1484
+ testCase.networkRequests = JSON.parse(net2.body.toString());
1370
1485
  } catch {
1371
1486
  }
1372
1487
  }
@@ -1422,17 +1537,17 @@ var FileHandler = class {
1422
1537
  const tracePaths = this.findTraceFiles(testCase);
1423
1538
  let lastPath = null;
1424
1539
  for (const tp of tracePaths) {
1425
- if (fs6.existsSync(tp)) lastPath = tp;
1540
+ if (fs7.existsSync(tp)) lastPath = tp;
1426
1541
  }
1427
1542
  if (!lastPath) return null;
1428
1543
  const hash = crypto3.createHash("sha256");
1429
- await new Promise((resolve9, reject) => {
1430
- 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);
1431
1546
  });
1432
1547
  return {
1433
1548
  tracePath: lastPath,
1434
1549
  hash: hash.digest("hex"),
1435
- size: fs6.statSync(lastPath).size
1550
+ size: fs7.statSync(lastPath).size
1436
1551
  };
1437
1552
  }
1438
1553
  /** Ask the server which trace hashes it already has, returning the set of hashes that are missing */
@@ -1459,14 +1574,14 @@ function formatMiB(bytes) {
1459
1574
  var import_node_child_process = require("child_process");
1460
1575
 
1461
1576
  // src/internal/support/test-paths.ts
1462
- var path7 = __toESM(require("path"));
1577
+ var path8 = __toESM(require("path"));
1463
1578
  function configDirFromConfig(config) {
1464
- if (config?.configFile) return path7.dirname(config.configFile);
1579
+ if (config?.configFile) return path8.dirname(config.configFile);
1465
1580
  if (config?.rootDir) return config.rootDir;
1466
1581
  return process.cwd();
1467
1582
  }
1468
1583
  function relativeTestPath(file, base) {
1469
- return path7.relative(base, file).split(path7.sep).join("/");
1584
+ return path8.relative(base, file).split(path8.sep).join("/");
1470
1585
  }
1471
1586
 
1472
1587
  // src/internal/collect/metadata-collector.ts
@@ -1526,11 +1641,6 @@ function resolveScmBaseBranch(env) {
1526
1641
  const trimmed = target?.trim();
1527
1642
  return trimmed ? normalizeRef(trimmed) : void 0;
1528
1643
  }
1529
- function detectCi(env) {
1530
- return Boolean(
1531
- 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"
1532
- );
1533
- }
1534
1644
  function resolveRunOrigin(env, warn) {
1535
1645
  const raw = env[PIWI_ORIGIN_ENV.kind]?.trim();
1536
1646
  let kind = detectCi(env) ? "ci" : "local";
@@ -1769,7 +1879,7 @@ var MetadataCollector = class {
1769
1879
  };
1770
1880
 
1771
1881
  // src/internal/streaming/stream-manager.ts
1772
- var fs8 = __toESM(require("fs"));
1882
+ var fs9 = __toESM(require("fs"));
1773
1883
 
1774
1884
  // src/internal/streaming/bounded-queue.ts
1775
1885
  var DROP_TIER = {
@@ -1908,10 +2018,10 @@ function createLimiter(maxConcurrent) {
1908
2018
  if (run) run();
1909
2019
  };
1910
2020
  return function limit(fn) {
1911
- return new Promise((resolve9, reject) => {
2021
+ return new Promise((resolve11, reject) => {
1912
2022
  const run = () => {
1913
2023
  active++;
1914
- Promise.resolve().then(fn).then(resolve9, reject).finally(next);
2024
+ Promise.resolve().then(fn).then(resolve11, reject).finally(next);
1915
2025
  };
1916
2026
  if (active < limitValue) run();
1917
2027
  else queue.push(run);
@@ -1920,18 +2030,18 @@ function createLimiter(maxConcurrent) {
1920
2030
  }
1921
2031
 
1922
2032
  // src/internal/support/setup-file.ts
1923
- var path8 = __toESM(require("path"));
2033
+ var path9 = __toESM(require("path"));
1924
2034
  var os6 = __toESM(require("os"));
1925
- var fs7 = __toESM(require("fs"));
2035
+ var fs8 = __toESM(require("fs"));
1926
2036
  function getSetupFilePath(projectName) {
1927
- return path8.join(os6.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
2037
+ return path9.join(os6.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
1928
2038
  }
1929
2039
  function readSetupInfo(projectName) {
1930
2040
  const setupFile = getSetupFilePath(projectName);
1931
2041
  try {
1932
- if (fs7.existsSync(setupFile)) {
1933
- const info = JSON.parse(fs7.readFileSync(setupFile, "utf8"));
1934
- fs7.unlinkSync(setupFile);
2042
+ if (fs8.existsSync(setupFile)) {
2043
+ const info = JSON.parse(fs8.readFileSync(setupFile, "utf8"));
2044
+ fs8.unlinkSync(setupFile);
1935
2045
  if (info.projectName === projectName) return info;
1936
2046
  }
1937
2047
  } catch {
@@ -2150,7 +2260,7 @@ var StreamManager = class {
2150
2260
  } catch (error) {
2151
2261
  if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
2152
2262
  this.logger.warn(
2153
- `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.`
2154
2264
  );
2155
2265
  } else {
2156
2266
  this.logger.warn(
@@ -2387,7 +2497,7 @@ var StreamManager = class {
2387
2497
  this.logger.debugError(
2388
2498
  `${this.pendingEvents.length} events pending, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`
2389
2499
  );
2390
- 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)));
2391
2501
  }
2392
2502
  if (!this.pendingEvents.isEmpty) {
2393
2503
  const remaining = this.pendingEvents.takeAll();
@@ -2431,7 +2541,7 @@ var StreamManager = class {
2431
2541
  }
2432
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. */
2433
2543
  scheduleLiveUpload(tc) {
2434
- 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));
2435
2545
  const hasAttachments = this.fileHandler.findAllAttachments(tc).length > 0;
2436
2546
  if (!hasTrace && !hasAttachments) return;
2437
2547
  const promise = (async () => {
@@ -2444,7 +2554,7 @@ var StreamManager = class {
2444
2554
  if (flush) await flush;
2445
2555
  const delays = [0, 1e3, 3e3];
2446
2556
  for (let attempt = 0; attempt < delays.length; attempt++) {
2447
- if (delays[attempt]) await new Promise((resolve9) => setTimeout(resolve9, delays[attempt]));
2557
+ if (delays[attempt]) await new Promise((resolve11) => setTimeout(resolve11, delays[attempt]));
2448
2558
  try {
2449
2559
  await this.limitLiveUpload(
2450
2560
  () => this.uploader.uploadCaseFiles(
@@ -3259,7 +3369,7 @@ function maskUrlFragment(hash) {
3259
3369
  }
3260
3370
 
3261
3371
  // ../core/src/locator-break.ts
3262
- var filePath = (path32) => path32.replace(/\\/g, "/").replace(/^\.\//, "");
3372
+ var filePath = (path34) => path34.replace(/\\/g, "/").replace(/^\.\//, "");
3263
3373
  function sameFilePath(a, b) {
3264
3374
  const x = filePath(a);
3265
3375
  const y = filePath(b);
@@ -3554,23 +3664,48 @@ function flattenSteps(steps, depth = 0) {
3554
3664
  }
3555
3665
  return result;
3556
3666
  }
3557
- 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 = []) {
3558
3691
  const flatSteps = flattenSteps(steps);
3559
3692
  if (errors) markRecoveredSteps(flatSteps, errors);
3560
- 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);
3561
3695
  let slowestStep = null;
3562
3696
  for (const s of flatSteps) {
3563
- 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 };
3564
3699
  }
3565
3700
  const navSteps = flatSteps.filter((s) => s.category === "navigation");
3566
3701
  const waitSteps = flatSteps.filter((s) => s.category === "wait");
3567
- const waitTotalDuration = waitSteps.reduce((sum, s) => sum + (s.duration || 0), 0);
3702
+ const waitTotalDuration = waitSteps.reduce((sum, s) => sum + (own(s) || 0), 0);
3568
3703
  return {
3569
3704
  steps: flatSteps,
3570
3705
  totalStepDuration,
3571
3706
  slowestStep,
3572
3707
  navigationCount: navSteps.length,
3573
- navigationTotalDuration: navSteps.reduce((sum, s) => sum + (s.duration || 0), 0),
3708
+ navigationTotalDuration: navSteps.reduce((sum, s) => sum + (own(s) || 0), 0),
3574
3709
  waitTotalDuration,
3575
3710
  waitCount: waitSteps.length
3576
3711
  };
@@ -3686,19 +3821,43 @@ function extractWaitEvents(steps, insideWait = false) {
3686
3821
  return events;
3687
3822
  }
3688
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
+
3689
3848
  // src/internal/support/reporter-version.ts
3690
- var fs9 = __toESM(require("fs"));
3691
- var path9 = __toESM(require("path"));
3849
+ var fs10 = __toESM(require("fs"));
3850
+ var path10 = __toESM(require("path"));
3692
3851
  var cachedVersion = null;
3693
3852
  var CANDIDATE_OFFSETS = ["..", "../../.."];
3694
3853
  function findOwnPackageJson(fromDir) {
3695
3854
  for (const offset of CANDIDATE_OFFSETS) {
3696
3855
  try {
3697
- const pkgPath = path9.resolve(fromDir, offset, "package.json");
3698
- 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"));
3699
3858
  const { name, version } = pkg;
3700
3859
  if (name === "@piwitests/reporter" && typeof version === "string")
3701
- return { version, root: path9.dirname(pkgPath) };
3860
+ return { version, root: path10.dirname(pkgPath) };
3702
3861
  } catch {
3703
3862
  }
3704
3863
  }
@@ -3711,11 +3870,11 @@ function getReporterVersion() {
3711
3870
  }
3712
3871
 
3713
3872
  // src/internal/support/source-snippet.ts
3714
- var fs10 = __toESM(require("fs"));
3715
- var path10 = __toESM(require("path"));
3873
+ var fs11 = __toESM(require("fs"));
3874
+ var path11 = __toESM(require("path"));
3716
3875
  function readSourceSnippet(file, declLine, context, failingLine) {
3717
3876
  try {
3718
- const content = fs10.readFileSync(file, "utf-8");
3877
+ const content = fs11.readFileSync(file, "utf-8");
3719
3878
  const lines = content.split("\n");
3720
3879
  const anchor = failingLine ?? declLine;
3721
3880
  const start = Math.max(0, anchor - context - 1);
@@ -3755,16 +3914,16 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
3755
3914
  if (picked.length >= maxFrames) break;
3756
3915
  let abs;
3757
3916
  try {
3758
- abs = path10.resolve(frame.file);
3917
+ abs = path11.resolve(frame.file);
3759
3918
  } catch {
3760
3919
  continue;
3761
3920
  }
3762
- const rel = path10.relative(projectRoot, abs);
3763
- if (rel.startsWith("..") || path10.isAbsolute(rel)) continue;
3921
+ const rel = path11.relative(projectRoot, abs);
3922
+ if (rel.startsWith("..") || path11.isAbsolute(rel)) continue;
3764
3923
  if (!isNaN(frame.line)) add(abs, frame.line);
3765
3924
  }
3766
3925
  }
3767
- const absTest = path10.resolve(testFile2);
3926
+ const absTest = path11.resolve(testFile2);
3768
3927
  if (!picked.some((p) => p.absFile === absTest)) {
3769
3928
  add(absTest, extractFailingLine(errorText, testFile2, declLine));
3770
3929
  }
@@ -3772,49 +3931,23 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
3772
3931
  for (const p of picked.slice(0, maxFrames)) {
3773
3932
  const snippet = readSourceSnippet(p.absFile, p.line, context, p.line);
3774
3933
  if (snippet) {
3775
- 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 });
3776
3935
  }
3777
3936
  }
3778
3937
  return frames;
3779
3938
  }
3780
3939
  function extractFailingLine(errorText, testFile2, declarationLine) {
3781
3940
  if (!errorText) return declarationLine;
3782
- const expectedFile = path10.resolve(testFile2);
3941
+ const expectedFile = path11.resolve(testFile2);
3783
3942
  for (const frame of stackFrames(errorText)) {
3784
3943
  try {
3785
- 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;
3786
3945
  } catch {
3787
3946
  }
3788
3947
  }
3789
3948
  return declarationLine;
3790
3949
  }
3791
3950
 
3792
- // src/internal/support/ci.ts
3793
- function detectCiIds(env) {
3794
- if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID) {
3795
- const run = env.GITHUB_RUN_ATTEMPT ? `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` : env.GITHUB_RUN_ID;
3796
- return { run, job: env.GITHUB_JOB || null };
3797
- }
3798
- if (env.GITLAB_CI && env.CI_PIPELINE_ID) return { run: env.CI_PIPELINE_ID, job: env.CI_JOB_ID || null };
3799
- if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID) return { run: env.CIRCLE_WORKFLOW_ID, job: env.CIRCLE_BUILD_NUM || null };
3800
- if (env.TRAVIS && env.TRAVIS_BUILD_ID) return { run: env.TRAVIS_BUILD_ID, job: env.TRAVIS_JOB_ID || null };
3801
- if (env.TF_BUILD && env.BUILD_BUILDID) return { run: env.BUILD_BUILDID, job: env.SYSTEM_JOBID || null };
3802
- if (env.JENKINS_URL && env.BUILD_ID) return { run: env.BUILD_ID, job: null };
3803
- if (env.BUILDKITE_BUILD_ID) return { run: env.BUILDKITE_BUILD_ID, job: env.BUILDKITE_JOB_ID || null };
3804
- if (env.TEAMCITY_BUILD_ID) return { run: env.TEAMCITY_BUILD_ID, job: null };
3805
- if (env.BITBUCKET_BUILD_NUMBER) return { run: env.BITBUCKET_BUILD_NUMBER, job: env.BITBUCKET_STEP_UUID || null };
3806
- if (env.SEMAPHORE_WORKFLOW_ID) return { run: env.SEMAPHORE_WORKFLOW_ID, job: env.SEMAPHORE_JOB_ID || null };
3807
- if (env.APPVEYOR_BUILD_ID) return { run: env.APPVEYOR_BUILD_ID, job: env.APPVEYOR_JOB_ID || null };
3808
- if (env.DRONE_BUILD_NUMBER) return { run: env.DRONE_BUILD_NUMBER, job: null };
3809
- return null;
3810
- }
3811
- function resolveRunLabel(configured, sharded) {
3812
- if (configured) return configured;
3813
- const ci = detectCiIds(process.env);
3814
- if (!ci) return null;
3815
- return !sharded && ci.job ? `${ci.run}|${ci.job}` : ci.run;
3816
- }
3817
-
3818
3951
  // src/internal/support/worker-index.ts
3819
3952
  function workerIndexOf(result) {
3820
3953
  if (!result) return null;
@@ -3900,28 +4033,28 @@ function isListMode(argv = process.argv) {
3900
4033
  }
3901
4034
 
3902
4035
  // src/public/global-setup.ts
3903
- var path14 = __toESM(require("path"));
3904
- var fs14 = __toESM(require("fs"));
4036
+ var path15 = __toESM(require("path"));
4037
+ var fs15 = __toESM(require("fs"));
3905
4038
 
3906
4039
  // src/internal/support/aria-sampling.ts
3907
- var path11 = __toESM(require("path"));
4040
+ var path12 = __toESM(require("path"));
3908
4041
  var os7 = __toESM(require("os"));
3909
- var fs11 = __toESM(require("fs"));
4042
+ var fs12 = __toESM(require("fs"));
3910
4043
  function ariaSampleIdentity(filePath2, title) {
3911
4044
  return `${filePath2}\0${title}`;
3912
4045
  }
3913
4046
  function getAriaSampleFilePath(projectName) {
3914
- 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`);
3915
4048
  }
3916
4049
  function writeAriaSampleFile(projectName, identities) {
3917
4050
  try {
3918
- fs11.writeFileSync(getAriaSampleFilePath(projectName), JSON.stringify({ projectName, identities }));
4051
+ fs12.writeFileSync(getAriaSampleFilePath(projectName), JSON.stringify({ projectName, identities }));
3919
4052
  } catch {
3920
4053
  }
3921
4054
  }
3922
4055
  function clearAriaSampleFile(projectName) {
3923
4056
  try {
3924
- fs11.rmSync(getAriaSampleFilePath(projectName), { force: true });
4057
+ fs12.rmSync(getAriaSampleFilePath(projectName), { force: true });
3925
4058
  } catch {
3926
4059
  }
3927
4060
  }
@@ -3930,7 +4063,7 @@ function loadAriaSampleSet(projectName) {
3930
4063
  if (cachedSets.has(projectName)) return cachedSets.get(projectName);
3931
4064
  let set = null;
3932
4065
  try {
3933
- const raw = fs11.readFileSync(getAriaSampleFilePath(projectName), "utf8");
4066
+ const raw = fs12.readFileSync(getAriaSampleFilePath(projectName), "utf8");
3934
4067
  const parsed2 = JSON.parse(raw);
3935
4068
  if (parsed2.projectName === projectName && Array.isArray(parsed2.identities)) {
3936
4069
  set = new Set(parsed2.identities.filter((x) => typeof x === "string"));
@@ -4060,7 +4193,7 @@ async function checkBaseUrl(target, options = {}) {
4060
4193
  } else {
4061
4194
  reason = outcome.error;
4062
4195
  }
4063
- if (attempt < attempts) await new Promise((resolve9) => setTimeout(resolve9, delayMs));
4196
+ if (attempt < attempts) await new Promise((resolve11) => setTimeout(resolve11, delayMs));
4064
4197
  }
4065
4198
  return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
4066
4199
  }
@@ -4084,14 +4217,14 @@ async function checkBaseUrls(config, options = {}) {
4084
4217
  }
4085
4218
 
4086
4219
  // src/internal/manifest/collect.ts
4087
- var fs12 = __toESM(require("fs"));
4088
- var path12 = __toESM(require("path"));
4220
+ var fs13 = __toESM(require("fs"));
4221
+ var path13 = __toESM(require("path"));
4089
4222
 
4090
4223
  // src/internal/transport/backend-request.ts
4091
4224
  var http2 = __toESM(require("http"));
4092
4225
  var https2 = __toESM(require("https"));
4093
4226
  function backendRequest(url, init) {
4094
- return new Promise((resolve9, reject) => {
4227
+ return new Promise((resolve11, reject) => {
4095
4228
  const target = new URL(url);
4096
4229
  const headers = { ...init.headers };
4097
4230
  if (init.body !== void 0) headers["Content-Length"] = Buffer.byteLength(init.body);
@@ -4106,7 +4239,7 @@ function backendRequest(url, init) {
4106
4239
  });
4107
4240
  res.on(
4108
4241
  "end",
4109
- () => 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") })
4110
4243
  );
4111
4244
  res.on("error", reject);
4112
4245
  res.on("close", () => {
@@ -4151,10 +4284,10 @@ function hasInstrumentationHeader(headers) {
4151
4284
  return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
4152
4285
  }
4153
4286
  function readCommittedManifest(configDir) {
4154
- const file = path12.join(configDir, COMMITTED_MANIFEST_FILE);
4287
+ const file = path13.join(configDir, COMMITTED_MANIFEST_FILE);
4155
4288
  try {
4156
- if (!fs12.existsSync(file)) return null;
4157
- return parseManifestJson(fs12.readFileSync(file, "utf8"));
4289
+ if (!fs13.existsSync(file)) return null;
4290
+ return parseManifestJson(fs13.readFileSync(file, "utf8"));
4158
4291
  } catch {
4159
4292
  return null;
4160
4293
  }
@@ -4180,8 +4313,8 @@ function backendManifestFetch(insecure) {
4180
4313
  function instrumentationManifestUrls(baseUrl) {
4181
4314
  const base = new URL(baseUrl);
4182
4315
  const root = new URL(INSTRUMENTATION_MANIFEST_PATH, base).toString();
4183
- const path32 = base.pathname.replace(/\/+$/, "");
4184
- 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];
4185
4318
  }
4186
4319
  async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
4187
4320
  try {
@@ -4319,13 +4452,13 @@ function normalizeFunctionRanges(ranges) {
4319
4452
  }
4320
4453
  function normalizeCoveragePath(raw) {
4321
4454
  if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
4322
- const path32 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4323
- if (!path32 || path32.startsWith("../") || path32.startsWith("/") || /^[A-Za-z]:\//.test(path32)) return null;
4324
- if (/(^|\/)node_modules\//.test(path32)) return null;
4325
- 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;
4326
4459
  }
4327
- function fileEntry(path32, runtime, blob, functions = [], lines = []) {
4328
- const file = { path: path32, runtime };
4460
+ function fileEntry(path34, runtime, blob, functions = [], lines = []) {
4461
+ const file = { path: path34, runtime };
4329
4462
  if (blob && (functions.length > 0 || lines.length > 0)) file.blob = blob;
4330
4463
  if (functions.length > 0) file.functions = functions;
4331
4464
  if (lines.length > 0) file.lines = lines;
@@ -4344,11 +4477,11 @@ function sanitizeCoverage(raw) {
4344
4477
  const files = [];
4345
4478
  for (const item of raw.files) {
4346
4479
  if (!isRecord(item) || !isRuntime(item.runtime)) continue;
4347
- const path32 = normalizeCoveragePath(item.path);
4348
- if (path32 === null) continue;
4480
+ const path34 = normalizeCoveragePath(item.path);
4481
+ if (path34 === null) continue;
4349
4482
  const blob = typeof item.blob === "string" && BLOB_ID.test(item.blob) ? item.blob : void 0;
4350
4483
  files.push(
4351
- fileEntry(path32, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
4484
+ fileEntry(path34, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
4352
4485
  );
4353
4486
  }
4354
4487
  const merged = mergeCoverage(captureOf(files, raw.incomplete === true));
@@ -4413,8 +4546,8 @@ function reduceCoverage(capture, granularity) {
4413
4546
 
4414
4547
  // src/internal/collect/source-checksums.ts
4415
4548
  var crypto4 = __toESM(require("crypto"));
4416
- var fs13 = __toESM(require("fs"));
4417
- var path13 = __toESM(require("path"));
4549
+ var fs14 = __toESM(require("fs"));
4550
+ var path14 = __toESM(require("path"));
4418
4551
  var CHECKSUM = /^(sha1|sha256):[0-9a-f]{40,64}$/;
4419
4552
  function checksumsOf(raw) {
4420
4553
  if (!Array.isArray(raw)) return [];
@@ -4479,10 +4612,10 @@ var SourceChecksums = class {
4479
4612
  }
4480
4613
  async read(relativePath2) {
4481
4614
  try {
4482
- const root = path13.resolve(this.repoRoot());
4483
- const file = path13.resolve(root, relativePath2);
4484
- if (!file.startsWith(root + path13.sep)) return null;
4485
- 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));
4486
4619
  } catch {
4487
4620
  return null;
4488
4621
  }
@@ -4627,13 +4760,13 @@ var ServerCoverageCollector = class {
4627
4760
  if (parts.length === 0 || this.closer.signal.aborted) {
4628
4761
  return Promise.resolve({ files: [], incomplete: truncated || parts.length > 0 });
4629
4762
  }
4630
- return new Promise((resolve9) => {
4763
+ return new Promise((resolve11) => {
4631
4764
  const entry = {
4632
4765
  pending: parts.length,
4633
4766
  captures: [],
4634
4767
  checksums: /* @__PURE__ */ new Map(),
4635
4768
  incomplete: truncated,
4636
- resolve: resolve9
4769
+ resolve: resolve11
4637
4770
  };
4638
4771
  for (const [origin, ids] of parts) this.enqueue(origin, { entry, ids });
4639
4772
  });
@@ -4959,14 +5092,14 @@ var ServerCoverageCollector = class {
4959
5092
  // src/public/global-setup.ts
4960
5093
  function createGlobalSetup(options, userSetup) {
4961
5094
  return async function globalSetupFn(config) {
4962
- const piwiReporterPath = path14.resolve(__dirname, "./index.js");
5095
+ const piwiReporterPath = path15.resolve(__dirname, "./index.js");
4963
5096
  let inlineReporterOptions = {};
4964
5097
  if (Array.isArray(config?.reporter)) {
4965
5098
  for (const r of config.reporter) {
4966
5099
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
4967
5100
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
4968
5101
  try {
4969
- return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
5102
+ return path15.resolve(require.resolve(r[0])) === piwiReporterPath;
4970
5103
  } catch {
4971
5104
  return false;
4972
5105
  }
@@ -4977,7 +5110,10 @@ function createGlobalSetup(options, userSetup) {
4977
5110
  }
4978
5111
  }
4979
5112
  }
4980
- const opts = resolveOptions({ ...inlineReporterOptions, ...options });
5113
+ const opts = resolveOptions(
5114
+ { ...inlineReporterOptions, ...options },
5115
+ configDirFromConfig(config)
5116
+ );
4981
5117
  const logger = new Logger(opts.verbose ?? false);
4982
5118
  if (isUiMode()) {
4983
5119
  logger.debug("UI mode detected \u2014 skipping run registration.");
@@ -5002,7 +5138,7 @@ function createGlobalSetup(options, userSetup) {
5002
5138
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
5003
5139
  if (r[0].toLowerCase().includes("piwi")) return true;
5004
5140
  try {
5005
- return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
5141
+ return path15.resolve(require.resolve(r[0])) === piwiReporterPath;
5006
5142
  } catch {
5007
5143
  return false;
5008
5144
  }
@@ -5032,7 +5168,7 @@ function createGlobalSetup(options, userSetup) {
5032
5168
  auth
5033
5169
  );
5034
5170
  if (response?.runId && response?.setupToken) {
5035
- fs14.writeFileSync(
5171
+ fs15.writeFileSync(
5036
5172
  getSetupFilePath(opts.projectName),
5037
5173
  JSON.stringify({
5038
5174
  runId: response.runId,
@@ -5086,11 +5222,11 @@ function createGlobalSetup(options, userSetup) {
5086
5222
  }
5087
5223
 
5088
5224
  // src/public/config-wrapper.ts
5089
- var path15 = __toESM(require("path"));
5090
- var fs17 = __toESM(require("fs"));
5225
+ var path16 = __toESM(require("path"));
5226
+ var fs18 = __toESM(require("fs"));
5091
5227
 
5092
5228
  // src/internal/probe/mode.ts
5093
- var fs15 = __toESM(require("fs"));
5229
+ var fs16 = __toESM(require("fs"));
5094
5230
 
5095
5231
  // src/internal/probe/plan.ts
5096
5232
  function probeRunMetadata(base = {}) {
@@ -5102,8 +5238,8 @@ function parseRouteKey(routeKey2) {
5102
5238
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
5103
5239
  }
5104
5240
  function patternToRegex(pattern) {
5105
- const path32 = pattern.split("?")[0] ?? pattern;
5106
- 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, "[^/]+");
5107
5243
  return new RegExp(`^${escaped}$`);
5108
5244
  }
5109
5245
  function suitePathEqual(a, b) {
@@ -5126,13 +5262,13 @@ function matchProbeItem(plan, test) {
5126
5262
  function requestMatchesRoute(method, url, routeKey2) {
5127
5263
  const { method: rm, pattern } = parseRouteKey(routeKey2);
5128
5264
  if (rm && rm !== method.toUpperCase()) return false;
5129
- let path32;
5265
+ let path34;
5130
5266
  try {
5131
- path32 = new URL(url).pathname;
5267
+ path34 = new URL(url).pathname;
5132
5268
  } catch {
5133
- path32 = url.split("?")[0] ?? url;
5269
+ path34 = url.split("?")[0] ?? url;
5134
5270
  }
5135
- return patternToRegex(pattern).test(path32);
5271
+ return patternToRegex(pattern).test(path34);
5136
5272
  }
5137
5273
  function createProbeState() {
5138
5274
  return { navigated: false, matches: 0 };
@@ -5154,10 +5290,10 @@ function isProbeMode() {
5154
5290
  function loadPlan() {
5155
5291
  if (planCache !== void 0) return planCache;
5156
5292
  planCache = null;
5157
- const path32 = process.env[PIWI_PROBE_ENV.plan];
5158
- if (path32) {
5293
+ const path34 = process.env[PIWI_PROBE_ENV.plan];
5294
+ if (path34) {
5159
5295
  try {
5160
- planCache = JSON.parse(fs15.readFileSync(path32, "utf8"));
5296
+ planCache = JSON.parse(fs16.readFileSync(path34, "utf8"));
5161
5297
  } catch {
5162
5298
  planCache = null;
5163
5299
  }
@@ -5181,17 +5317,17 @@ function outcomeFromStatus(status, applied) {
5181
5317
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
5182
5318
  }
5183
5319
  function recordProbeOutcome(line) {
5184
- const path32 = process.env[PIWI_PROBE_ENV.results];
5185
- if (!path32) return;
5320
+ const path34 = process.env[PIWI_PROBE_ENV.results];
5321
+ if (!path34) return;
5186
5322
  try {
5187
- fs15.appendFileSync(path32, `${JSON.stringify(line)}
5323
+ fs16.appendFileSync(path34, `${JSON.stringify(line)}
5188
5324
  `);
5189
5325
  } catch {
5190
5326
  }
5191
5327
  }
5192
5328
 
5193
5329
  // src/internal/flake/mode.ts
5194
- var fs16 = __toESM(require("fs"));
5330
+ var fs17 = __toESM(require("fs"));
5195
5331
 
5196
5332
  // ../core/src/error-signature.ts
5197
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;
@@ -5567,7 +5703,7 @@ function originOf2(url) {
5567
5703
  return null;
5568
5704
  }
5569
5705
  }
5570
- 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)));
5571
5707
  async function performRouteAction(route, action, startedAt) {
5572
5708
  switch (action.kind) {
5573
5709
  case "abort":
@@ -5691,7 +5827,7 @@ function loadFlakePlan() {
5691
5827
  if (planCache2 && planCache2.file === file) return planCache2.plan;
5692
5828
  let text;
5693
5829
  try {
5694
- text = fs16.readFileSync(file, "utf8");
5830
+ text = fs17.readFileSync(file, "utf8");
5695
5831
  } catch (error) {
5696
5832
  throw new Error(`[Piwi Dashboard] Cannot read the flake plan ${file}: ${errorMessage(error)}`);
5697
5833
  }
@@ -5852,7 +5988,7 @@ function recordFlakeResult(line) {
5852
5988
  const file = process.env[PIWI_FLAKE_ENV.results]?.trim();
5853
5989
  if (!file) return;
5854
5990
  try {
5855
- fs16.appendFileSync(file, `${JSON.stringify(line)}
5991
+ fs17.appendFileSync(file, `${JSON.stringify(line)}
5856
5992
  `);
5857
5993
  } catch {
5858
5994
  }
@@ -5923,11 +6059,11 @@ function injectReporter(reporter, piwiOptions) {
5923
6059
  }
5924
6060
  function resolveSetupModule() {
5925
6061
  const candidates = [
5926
- path15.join(__dirname, "global-setup-module.js"),
5927
- path15.join(__dirname, "..", "global-setup-module.js"),
5928
- 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")
5929
6065
  ];
5930
- return candidates.find((candidate) => fs17.existsSync(candidate)) ?? candidates[0];
6066
+ return candidates.find((candidate) => fs18.existsSync(candidate)) ?? candidates[0];
5931
6067
  }
5932
6068
  function wrapConfig(config, piwiOptions) {
5933
6069
  if (piwiOptions) applyOptionsToEnv(piwiOptions);
@@ -6323,8 +6459,8 @@ function buildErrorText(result, base = process.cwd()) {
6323
6459
  var import_node_crypto2 = require("crypto");
6324
6460
 
6325
6461
  // src/internal/support/ci-output.ts
6326
- var fs18 = __toESM(require("fs"));
6327
- var path16 = __toESM(require("path"));
6462
+ var fs19 = __toESM(require("fs"));
6463
+ var path17 = __toESM(require("path"));
6328
6464
  var SUMMARY_MAX_FAILURES = 20;
6329
6465
  function emitRunOutputs(output, logger, outputFile, env = process.env) {
6330
6466
  logger.info(`View run: ${output.runUrl}`);
@@ -6334,9 +6470,9 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
6334
6470
  }
6335
6471
  function writeOutputFile(file, output, logger) {
6336
6472
  try {
6337
- const dir = path16.dirname(file);
6338
- if (dir && dir !== ".") fs18.mkdirSync(dir, { recursive: true });
6339
- fs18.writeFileSync(
6473
+ const dir = path17.dirname(file);
6474
+ if (dir && dir !== ".") fs19.mkdirSync(dir, { recursive: true });
6475
+ fs19.writeFileSync(
6340
6476
  file,
6341
6477
  JSON.stringify(
6342
6478
  {
@@ -6416,7 +6552,7 @@ function emitGitLabDotenv(output, env, logger) {
6416
6552
  if (output.projectId != null) lines.push(`PIWI_PROJECT_ID=${output.projectId}`);
6417
6553
  if (output.ciBuildUrl) lines.push(`PIWI_CI_BUILD_URL=${output.ciBuildUrl}`);
6418
6554
  try {
6419
- fs18.writeFileSync(file, lines.join("\n") + "\n");
6555
+ fs19.writeFileSync(file, lines.join("\n") + "\n");
6420
6556
  logger.info(`Wrote GitLab dotenv report to ${file} (declare it as artifacts:reports:dotenv)`);
6421
6557
  } catch (error) {
6422
6558
  logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
@@ -6433,7 +6569,7 @@ function emitResourceSummary(lines, logger, env = process.env) {
6433
6569
  }
6434
6570
  function appendFileLines(file, lines, logger, label) {
6435
6571
  try {
6436
- fs18.appendFileSync(file, lines.join("\n") + "\n");
6572
+ fs19.appendFileSync(file, lines.join("\n") + "\n");
6437
6573
  } catch (error) {
6438
6574
  logger.warn(`Failed to write GitHub Actions ${label}: ${errorMessage(error)}`);
6439
6575
  }
@@ -6446,8 +6582,8 @@ function ciBuildUrlFromMetadata(metadata) {
6446
6582
 
6447
6583
  // src/internal/submit/run-submitter.ts
6448
6584
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
6449
- function markSubmitFallback(run, path32, reason) {
6450
- run.metadata.ingestHealth = { submitFallback: { path: path32, reason } };
6585
+ function markSubmitFallback(run, path34, reason) {
6586
+ run.metadata.ingestHealth = { submitFallback: { path: path34, reason } };
6451
6587
  }
6452
6588
  var RunSubmitter = class {
6453
6589
  /**
@@ -6708,7 +6844,7 @@ var RunSubmitter = class {
6708
6844
  */
6709
6845
  stopForMissingAuth(run, payload) {
6710
6846
  this.logger.error(
6711
- `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.`
6712
6848
  );
6713
6849
  this.logRecoverySaved();
6714
6850
  this.saveRecovery(payload);
@@ -6723,13 +6859,13 @@ var RunSubmitter = class {
6723
6859
  };
6724
6860
 
6725
6861
  // src/internal/capture/resource-ledger.ts
6726
- var fs20 = __toESM(require("fs"));
6727
- var path19 = __toESM(require("path"));
6862
+ var fs21 = __toESM(require("fs"));
6863
+ var path20 = __toESM(require("path"));
6728
6864
  var import_node_module2 = require("module");
6729
6865
 
6730
6866
  // src/internal/capture/quiet-capture.ts
6731
6867
  var import_node_module = require("module");
6732
- var path17 = __toESM(require("path"));
6868
+ var path18 = __toESM(require("path"));
6733
6869
  var BOXED_PREFIX_EXPORTS = [
6734
6870
  [
6735
6871
  "playwright-core/lib/coreBundle",
@@ -6766,7 +6902,7 @@ function resolveBoxing() {
6766
6902
  if (typeof set !== "function") continue;
6767
6903
  return {
6768
6904
  set,
6769
- prefixes: [path17.dirname(playwrightPackageJson), path17.join(root, "dist") + path17.sep]
6905
+ prefixes: [path18.dirname(playwrightPackageJson), path18.join(root, "dist") + path18.sep]
6770
6906
  };
6771
6907
  }
6772
6908
  } catch {
@@ -6798,12 +6934,12 @@ function internalCall(owner, run) {
6798
6934
  var import_node_perf_hooks = require("perf_hooks");
6799
6935
 
6800
6936
  // src/internal/support/system-readers.ts
6801
- var fs19 = __toESM(require("fs"));
6802
- var path18 = __toESM(require("path"));
6937
+ var fs20 = __toESM(require("fs"));
6938
+ var path19 = __toESM(require("path"));
6803
6939
  var CLK_TCK = 100;
6804
6940
  function readText(file) {
6805
6941
  try {
6806
- return fs19.readFileSync(file, "utf8");
6942
+ return fs20.readFileSync(file, "utf8");
6807
6943
  } catch {
6808
6944
  return null;
6809
6945
  }
@@ -6937,45 +7073,45 @@ var ProcFs = class {
6937
7073
  }
6938
7074
  /** Whether this looks like a Linux `/proc` at all. */
6939
7075
  available() {
6940
- return fs19.existsSync(path18.join(this.root, "stat"));
7076
+ return fs20.existsSync(path19.join(this.root, "stat"));
6941
7077
  }
6942
7078
  /** Every process, keyed by pid. */
6943
7079
  table() {
6944
7080
  const out = /* @__PURE__ */ new Map();
6945
7081
  let names;
6946
7082
  try {
6947
- names = fs19.readdirSync(this.root);
7083
+ names = fs20.readdirSync(this.root);
6948
7084
  } catch {
6949
7085
  return out;
6950
7086
  }
6951
7087
  for (const name of names) {
6952
7088
  if (!/^\d+$/.test(name)) continue;
6953
- const text = readText(path18.join(this.root, name, "stat"));
7089
+ const text = readText(path19.join(this.root, name, "stat"));
6954
7090
  const stat = text ? parseProcStat(Number(name), text) : null;
6955
7091
  if (stat) out.set(stat.pid, stat);
6956
7092
  }
6957
7093
  return out;
6958
7094
  }
6959
7095
  stat(pid) {
6960
- const text = readText(path18.join(this.root, String(pid), "stat"));
7096
+ const text = readText(path19.join(this.root, String(pid), "stat"));
6961
7097
  return text ? parseProcStat(pid, text) : null;
6962
7098
  }
6963
7099
  runWaitMs(pid) {
6964
- const text = readText(path18.join(this.root, String(pid), "schedstat"));
7100
+ const text = readText(path19.join(this.root, String(pid), "schedstat"));
6965
7101
  return text ? parseSchedstat(text) : null;
6966
7102
  }
6967
7103
  cmdline(pid) {
6968
- 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();
6969
7105
  }
6970
7106
  /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
6971
7107
  statusKb(pid, key) {
6972
- const text = readText(path18.join(this.root, String(pid), "status"));
7108
+ const text = readText(path19.join(this.root, String(pid), "status"));
6973
7109
  return text ? parseKbField(text, key) : null;
6974
7110
  }
6975
7111
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
6976
7112
  async pssKb(pid) {
6977
7113
  try {
6978
- 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");
6979
7115
  } catch {
6980
7116
  return null;
6981
7117
  }
@@ -6983,29 +7119,29 @@ var ProcFs = class {
6983
7119
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
6984
7120
  resetPeakRss(pid) {
6985
7121
  try {
6986
- fs19.writeFileSync(path18.join(this.root, String(pid), "clear_refs"), "5");
7122
+ fs20.writeFileSync(path19.join(this.root, String(pid), "clear_refs"), "5");
6987
7123
  return true;
6988
7124
  } catch {
6989
7125
  return false;
6990
7126
  }
6991
7127
  }
6992
7128
  cpuTicks() {
6993
- const text = readText(path18.join(this.root, "stat"));
7129
+ const text = readText(path19.join(this.root, "stat"));
6994
7130
  return text ? parseCpuTicks(text) : null;
6995
7131
  }
6996
7132
  /** Total stall time of a resource's pressure file, in µs. */
6997
7133
  pressureUs(resource) {
6998
- const text = readText(path18.join(this.root, "pressure", resource));
7134
+ const text = readText(path19.join(this.root, "pressure", resource));
6999
7135
  return text ? parsePsiTotal(text) : null;
7000
7136
  }
7001
7137
  memAvailableKb() {
7002
- const text = readText(path18.join(this.root, "meminfo"));
7138
+ const text = readText(path19.join(this.root, "meminfo"));
7003
7139
  return text ? parseKbField(text, "MemAvailable") : null;
7004
7140
  }
7005
7141
  /** Open file descriptors of a process. */
7006
7142
  fdCount(pid) {
7007
7143
  try {
7008
- return fs19.readdirSync(path18.join(this.root, String(pid), "fd")).length;
7144
+ return fs20.readdirSync(path19.join(this.root, String(pid), "fd")).length;
7009
7145
  } catch {
7010
7146
  return null;
7011
7147
  }
@@ -7026,7 +7162,7 @@ var CgroupFs = class {
7026
7162
  }
7027
7163
  /** The directory of a controller (v1) or of the unified hierarchy (v2), when it exists. */
7028
7164
  dirs() {
7029
- const text = readText(path18.join(this.procRoot, String(this.pid), "cgroup"));
7165
+ const text = readText(path19.join(this.procRoot, String(this.pid), "cgroup"));
7030
7166
  if (!text) return null;
7031
7167
  const v1 = /* @__PURE__ */ new Map();
7032
7168
  let v2 = null;
@@ -7037,21 +7173,21 @@ var CgroupFs = class {
7037
7173
  else for (const controller of match[1].split(",")) v1.set(controller, match[2]);
7038
7174
  }
7039
7175
  const pick = (mount, sub) => {
7040
- const nested = path18.join(mount, sub);
7041
- if (fs19.existsSync(nested)) return nested;
7042
- 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;
7043
7179
  };
7044
7180
  if (v1.has("memory") || v1.has("cpu")) {
7045
7181
  const memoryPath = v1.get("memory");
7046
7182
  const cpuPath = v1.get("cpu");
7047
- 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));
7048
7184
  return {
7049
7185
  version: 1,
7050
- 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,
7051
7187
  cpu: cpuPath !== void 0 && cpuMount ? pick(cpuMount, cpuPath) : null
7052
7188
  };
7053
7189
  }
7054
- if (v2 !== null && fs19.existsSync(path18.join(this.cgroupRoot, "cgroup.controllers"))) {
7190
+ if (v2 !== null && fs20.existsSync(path19.join(this.cgroupRoot, "cgroup.controllers"))) {
7055
7191
  const dir = pick(this.cgroupRoot, v2);
7056
7192
  return { version: 2, memory: dir, cpu: dir };
7057
7193
  }
@@ -7060,7 +7196,7 @@ var CgroupFs = class {
7060
7196
  read() {
7061
7197
  const dirs = this.dirs();
7062
7198
  if (!dirs) return null;
7063
- const file = (dir, name) => dir ? readText(path18.join(dir, name)) : null;
7199
+ const file = (dir, name) => dir ? readText(path19.join(dir, name)) : null;
7064
7200
  if (dirs.version === 2) {
7065
7201
  const max = file(dirs.memory, "memory.max");
7066
7202
  const cpuMax = file(dirs.cpu, "cpu.max")?.trim().split(/\s+/);
@@ -7246,10 +7382,10 @@ var PAGE_READ_TIMEOUT_MS = 1e3;
7246
7382
  var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
7247
7383
  var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
7248
7384
  function relativePath(file) {
7249
- 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("/");
7250
7386
  }
7251
7387
  function isLibraryFile(file) {
7252
- 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;
7253
7389
  }
7254
7390
  function userSite(stack) {
7255
7391
  for (const line of (stack ?? "").split("\n").slice(1)) {
@@ -7297,7 +7433,7 @@ function playwrightTestInfoSource() {
7297
7433
  try {
7298
7434
  const packageJson = resolvePlaywrightPackageJson();
7299
7435
  if (!packageJson) return () => null;
7300
- 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"));
7301
7437
  const read = globals.currentTestInfo;
7302
7438
  if (typeof read !== "function") return () => null;
7303
7439
  return () => {
@@ -7326,15 +7462,15 @@ function countHandles() {
7326
7462
  async function settledHandles(start) {
7327
7463
  const end = countHandles();
7328
7464
  if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
7329
- await new Promise((resolve9) => setTimeout(resolve9, 0));
7465
+ await new Promise((resolve11) => setTimeout(resolve11, 0));
7330
7466
  return countHandles();
7331
7467
  }
7332
7468
  function settleWithin(promise, ms) {
7333
7469
  let timer;
7334
7470
  return Promise.race([
7335
7471
  promise.catch(() => void 0),
7336
- new Promise((resolve9) => {
7337
- timer = setTimeout(resolve9, ms);
7472
+ new Promise((resolve11) => {
7473
+ timer = setTimeout(resolve11, ms);
7338
7474
  })
7339
7475
  ]).finally(() => clearTimeout(timer));
7340
7476
  }
@@ -7463,7 +7599,7 @@ var ResourceLedger = class {
7463
7599
  const census = this.census(null);
7464
7600
  if (this.resultsFile) {
7465
7601
  try {
7466
- fs20.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
7602
+ fs21.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
7467
7603
  `);
7468
7604
  } catch {
7469
7605
  }
@@ -7786,7 +7922,7 @@ async function recordResourceCensus(testInfo) {
7786
7922
  }
7787
7923
 
7788
7924
  // src/internal/collect/resource-verdicts.ts
7789
- var path20 = __toESM(require("path"));
7925
+ var path21 = __toESM(require("path"));
7790
7926
  var OPEN_STEP_TITLES = {
7791
7927
  "Create context": "context",
7792
7928
  "Launch persistent context": "context",
@@ -7818,7 +7954,7 @@ function userFixturesOf(steps) {
7818
7954
  const walk = (list) => {
7819
7955
  for (const step of list) {
7820
7956
  const file = step.location?.file;
7821
- 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}`)) {
7822
7958
  const name = step.title.replace(/^(?:Fixture\s+|fixture:\s*)/, "").replace(/^"(.*)"$/, "$1");
7823
7959
  if (!name.startsWith("piwi")) out.push(`${name} (${relativeFile(file)}:${step.location.line})`);
7824
7960
  }
@@ -7851,7 +7987,7 @@ function tallyLifecycleSteps(file, steps) {
7851
7987
  return tally;
7852
7988
  }
7853
7989
  function relativeFile(file) {
7854
- return path20.relative(process.cwd(), file).split(path20.sep).join("/");
7990
+ return path21.relative(process.cwd(), file).split(path21.sep).join("/");
7855
7991
  }
7856
7992
  function buildResourceReport(input) {
7857
7993
  const findings = [];
@@ -8209,9 +8345,9 @@ function formatResourceSummary(report, leakCheck, maxLines = 10) {
8209
8345
  }
8210
8346
 
8211
8347
  // src/internal/collect/process-sampler.ts
8212
- var fs21 = __toESM(require("fs"));
8348
+ var fs22 = __toESM(require("fs"));
8213
8349
  var os8 = __toESM(require("os"));
8214
- var path21 = __toESM(require("path"));
8350
+ var path22 = __toESM(require("path"));
8215
8351
  var import_node_child_process2 = require("child_process");
8216
8352
  var WALK_FILE_CAP = 1e4;
8217
8353
  var SERIES_CAP = 3600;
@@ -8251,17 +8387,17 @@ function nodeCpuTicks() {
8251
8387
  return { busy, idle, iowait: 0, steal: 0, total: busy + idle };
8252
8388
  }
8253
8389
  function defaultPs() {
8254
- return new Promise((resolve9, reject) => {
8390
+ return new Promise((resolve11, reject) => {
8255
8391
  (0, import_node_child_process2.execFile)(
8256
8392
  "ps",
8257
8393
  ["-A", "-o", "pid=,ppid=,rss=,time=,command="],
8258
8394
  { timeout: 2e3, maxBuffer: 8 * 1024 * 1024 },
8259
- (error, stdout) => error ? reject(error) : resolve9(stdout)
8395
+ (error, stdout) => error ? reject(error) : resolve11(stdout)
8260
8396
  );
8261
8397
  });
8262
8398
  }
8263
8399
  function defaultWindowsQuery() {
8264
- return new Promise((resolve9, reject) => {
8400
+ return new Promise((resolve11, reject) => {
8265
8401
  (0, import_node_child_process2.execFile)(
8266
8402
  "powershell.exe",
8267
8403
  [
@@ -8271,18 +8407,18 @@ function defaultWindowsQuery() {
8271
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"
8272
8408
  ],
8273
8409
  { timeout: 1e4, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
8274
- (error, stdout) => error ? reject(error) : resolve9(stdout)
8410
+ (error, stdout) => error ? reject(error) : resolve11(stdout)
8275
8411
  );
8276
8412
  });
8277
8413
  }
8278
8414
  function freeBytes(dir) {
8279
- let current = path21.resolve(dir);
8415
+ let current = path22.resolve(dir);
8280
8416
  for (; ; ) {
8281
8417
  try {
8282
- const stats = fs21.statfsSync(current);
8418
+ const stats = fs22.statfsSync(current);
8283
8419
  return stats.bavail * stats.bsize;
8284
8420
  } catch {
8285
- const parent = path21.dirname(current);
8421
+ const parent = path22.dirname(current);
8286
8422
  if (parent === current) return null;
8287
8423
  current = parent;
8288
8424
  }
@@ -8292,18 +8428,18 @@ async function dirBytes(dir, budget) {
8292
8428
  let bytes = 0;
8293
8429
  let entries;
8294
8430
  try {
8295
- entries = await fs21.promises.readdir(dir, { withFileTypes: true });
8431
+ entries = await fs22.promises.readdir(dir, { withFileTypes: true });
8296
8432
  } catch {
8297
8433
  return 0;
8298
8434
  }
8299
8435
  for (const entry of entries) {
8300
8436
  if (budget.files <= 0) return bytes;
8301
- const full = path21.join(dir, entry.name);
8437
+ const full = path22.join(dir, entry.name);
8302
8438
  if (entry.isDirectory()) bytes += await dirBytes(full, budget);
8303
8439
  else if (entry.isFile()) {
8304
8440
  budget.files--;
8305
8441
  try {
8306
- bytes += (await fs21.promises.lstat(full)).size;
8442
+ bytes += (await fs22.promises.lstat(full)).size;
8307
8443
  } catch {
8308
8444
  }
8309
8445
  }
@@ -8343,7 +8479,7 @@ var RunSampler = class {
8343
8479
  this.platform = options.platform ?? process.platform;
8344
8480
  this.proc = new ProcFs(options.procRoot);
8345
8481
  this.cgroup = new CgroupFs(options.procRoot, options.cgroupRoot);
8346
- this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path21.resolve(dir)))];
8482
+ this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path22.resolve(dir)))];
8347
8483
  this.tmpDir = options.tmpDir ?? os8.tmpdir();
8348
8484
  this.intervalMs = options.intervalMs ?? 1e3;
8349
8485
  this.heavyEvery = Math.max(1, options.heavyEvery ?? 5);
@@ -8545,7 +8681,7 @@ var RunSampler = class {
8545
8681
  } catch {
8546
8682
  return;
8547
8683
  }
8548
- 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);
8549
8685
  }
8550
8686
  async sampleWindowsTree(at) {
8551
8687
  let list;
@@ -8601,7 +8737,7 @@ var RunSampler = class {
8601
8737
  let bytes = 0;
8602
8738
  for (const dir of this.outputDirs) bytes += await dirBytes(dir, budget);
8603
8739
  for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs >= this.startedAt - 1e3)) {
8604
- bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
8740
+ bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
8605
8741
  }
8606
8742
  if (budget.files <= 0) this.peakInUseIsLowerBound = true;
8607
8743
  this.peakInUse = Math.max(this.peakInUse ?? 0, bytes);
@@ -8610,7 +8746,7 @@ var RunSampler = class {
8610
8746
  async playwrightTempEntries(keep) {
8611
8747
  let names;
8612
8748
  try {
8613
- names = await fs21.promises.readdir(this.tmpDir);
8749
+ names = await fs22.promises.readdir(this.tmpDir);
8614
8750
  } catch {
8615
8751
  return [];
8616
8752
  }
@@ -8618,7 +8754,7 @@ var RunSampler = class {
8618
8754
  for (const name of names) {
8619
8755
  if (!PLAYWRIGHT_TEMP.test(name)) continue;
8620
8756
  try {
8621
- 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);
8622
8758
  } catch {
8623
8759
  }
8624
8760
  }
@@ -8629,7 +8765,7 @@ var RunSampler = class {
8629
8765
  const budget = { files: WALK_FILE_CAP };
8630
8766
  let bytes = 0;
8631
8767
  for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs < this.startedAt - 1e3)) {
8632
- bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
8768
+ bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
8633
8769
  }
8634
8770
  return bytes;
8635
8771
  } catch {
@@ -9603,7 +9739,7 @@ function browserFunctionsInWorker() {
9603
9739
  function browserFunctionCoverage(files) {
9604
9740
  return sanitizeCoverage({
9605
9741
  v: 1,
9606
- files: [...files].map(([path32, functions]) => ({ path: path32, runtime: "browser", functions }))
9742
+ files: [...files].map(([path34, functions]) => ({ path: path34, runtime: "browser", functions }))
9607
9743
  });
9608
9744
  }
9609
9745
  function newKnownFunctions(known, sent) {
@@ -9616,19 +9752,19 @@ function newKnownFunctions(known, sent) {
9616
9752
  budget = MAX_COVERAGE_RANGES;
9617
9753
  };
9618
9754
  for (const [raw, ranges] of known) {
9619
- const path32 = normalizeCoveragePath(raw);
9620
- if (path32 === null) continue;
9621
- let named = sent.get(path32);
9755
+ const path34 = normalizeCoveragePath(raw);
9756
+ if (path34 === null) continue;
9757
+ let named = sent.get(path34);
9622
9758
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
9623
9759
  if (fresh.length === 0) continue;
9624
- if (!named) sent.set(path32, named = /* @__PURE__ */ new Set());
9760
+ if (!named) sent.set(path34, named = /* @__PURE__ */ new Set());
9625
9761
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
9626
9762
  while (fresh.length > 0) {
9627
9763
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
9628
9764
  const part = fresh.slice(0, budget);
9629
9765
  fresh = fresh.slice(part.length);
9630
9766
  budget -= part.length;
9631
- files.push({ path: path32, allFunctions: part });
9767
+ files.push({ path: path34, allFunctions: part });
9632
9768
  }
9633
9769
  }
9634
9770
  close();
@@ -9640,8 +9776,8 @@ function serverCoverageInWorker() {
9640
9776
 
9641
9777
  // src/internal/capture/code-reach.ts
9642
9778
  var crypto5 = __toESM(require("crypto"));
9643
- var fs22 = __toESM(require("fs"));
9644
- var path22 = __toESM(require("path"));
9779
+ var fs23 = __toESM(require("fs"));
9780
+ var path23 = __toESM(require("path"));
9645
9781
  var import_node_child_process3 = require("child_process");
9646
9782
 
9647
9783
  // ../core/src/source-map.ts
@@ -9986,18 +10122,18 @@ function repositoryRoot(from) {
9986
10122
  }).trim();
9987
10123
  } catch {
9988
10124
  }
9989
- cachedRepoRoot = { from, root: path22.resolve(root) };
10125
+ cachedRepoRoot = { from, root: path23.resolve(root) };
9990
10126
  return cachedRepoRoot.root;
9991
10127
  }
9992
10128
  function codeReachRoots(configDir, configured) {
9993
10129
  const repoRoot = repositoryRoot(configDir);
9994
- const resolve9 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path22.resolve(configDir, r));
9995
- 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?.["*"]);
9996
10132
  const byOrigin = /* @__PURE__ */ new Map();
9997
10133
  if (configured && !Array.isArray(configured)) {
9998
10134
  for (const [key, dirs] of Object.entries(configured)) {
9999
10135
  const origin = key === "*" ? null : originOf3(key);
10000
- const resolved = resolve9(dirs);
10136
+ const resolved = resolve11(dirs);
10001
10137
  if (origin && resolved.length) byOrigin.set(origin, resolved);
10002
10138
  }
10003
10139
  }
@@ -10020,7 +10156,7 @@ function scriptRoots(roots, scriptUrl, pageUrl) {
10020
10156
  }
10021
10157
  function isFile(file) {
10022
10158
  try {
10023
- return fs22.statSync(file).isFile();
10159
+ return fs23.statSync(file).isFile();
10024
10160
  } catch {
10025
10161
  return false;
10026
10162
  }
@@ -10028,10 +10164,10 @@ function isFile(file) {
10028
10164
  function resolveSourcePath(source, roots) {
10029
10165
  const normalized = normalizeSourcePath(source);
10030
10166
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
10031
- if (path22.isAbsolute(normalized) && isFile(normalized)) return path22.resolve(normalized);
10167
+ if (path23.isAbsolute(normalized) && isFile(normalized)) return path23.resolve(normalized);
10032
10168
  const relative11 = normalized.replace(/^\/+/, "");
10033
10169
  for (const root of roots) {
10034
- const candidate = path22.resolve(root, relative11);
10170
+ const candidate = path23.resolve(root, relative11);
10035
10171
  if (isFile(candidate)) return candidate;
10036
10172
  }
10037
10173
  return null;
@@ -10063,7 +10199,7 @@ function fileForUrl(url, roots, bases) {
10063
10199
  return file;
10064
10200
  }
10065
10201
  function relativeToRepo(file, repoRoot) {
10066
- return path22.relative(repoRoot, file).split(path22.sep).join("/");
10202
+ return path23.relative(repoRoot, file).split(path23.sep).join("/");
10067
10203
  }
10068
10204
  function scriptVersion(entry) {
10069
10205
  return entry.hash ?? crypto5.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -10120,10 +10256,10 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
10120
10256
  const files = /* @__PURE__ */ new Map();
10121
10257
  const known = /* @__PURE__ */ new Map();
10122
10258
  const addTo = (into, file, functions) => {
10123
- const path32 = relativeToRepo(file, roots.repoRoot);
10124
- const listed = into.get(path32);
10259
+ const path34 = relativeToRepo(file, roots.repoRoot);
10260
+ const listed = into.get(path34);
10125
10261
  if (listed) listed.push(...functions);
10126
- else into.set(path32, [...functions]);
10262
+ else into.set(path34, [...functions]);
10127
10263
  };
10128
10264
  const add = (file, functions) => addTo(files, file, functions);
10129
10265
  const bases = viteBases(entries);
@@ -10275,8 +10411,8 @@ function onScriptParsed(page, capture, event) {
10275
10411
  var READ_WAIT_MS = 5e3;
10276
10412
  function readCoverage(page, cdp) {
10277
10413
  let timer;
10278
- const late = new Promise((resolve9) => {
10279
- timer = setTimeout(() => resolve9([]), READ_WAIT_MS);
10414
+ const late = new Promise((resolve11) => {
10415
+ timer = setTimeout(() => resolve11([]), READ_WAIT_MS);
10280
10416
  });
10281
10417
  const taken = internalCall(page, () => cdp.send("Profiler.takePreciseCoverage")).then(
10282
10418
  (answer) => answer.result,
@@ -10388,8 +10524,8 @@ async function stopCodeReach(page) {
10388
10524
  // src/internal/collect/blob-ids.ts
10389
10525
  var import_node_child_process4 = require("child_process");
10390
10526
  var crypto6 = __toESM(require("crypto"));
10391
- var fs23 = __toESM(require("fs"));
10392
- var path23 = __toESM(require("path"));
10527
+ var fs24 = __toESM(require("fs"));
10528
+ var path24 = __toESM(require("path"));
10393
10529
  function gitBlobId(content) {
10394
10530
  return crypto6.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
10395
10531
  }
@@ -10398,14 +10534,14 @@ function namesLines(file) {
10398
10534
  }
10399
10535
  var GIT_BATCH = 500;
10400
10536
  function gitHashObjects(root, paths) {
10401
- return new Promise((resolve9) => {
10537
+ return new Promise((resolve11) => {
10402
10538
  const child = (0, import_node_child_process4.execFile)(
10403
10539
  "git",
10404
10540
  ["hash-object", "--stdin-paths"],
10405
10541
  { cwd: root, encoding: "utf8", timeout: 3e4, windowsHide: true },
10406
10542
  (error, stdout) => {
10407
10543
  const ids = error ? [] : stdout.split("\n").filter(Boolean);
10408
- resolve9(ids.length === paths.length ? ids : null);
10544
+ resolve11(ids.length === paths.length ? ids : null);
10409
10545
  }
10410
10546
  );
10411
10547
  child.stdin?.on("error", () => {
@@ -10456,15 +10592,15 @@ var BlobIds = class {
10456
10592
  async read(relativePaths) {
10457
10593
  let root;
10458
10594
  try {
10459
- root = path23.resolve(this.repoRoot());
10595
+ root = path24.resolve(this.repoRoot());
10460
10596
  } catch {
10461
10597
  return relativePaths.map(() => null);
10462
10598
  }
10463
10599
  const files = await Promise.all(
10464
10600
  relativePaths.map(async (relativePath2) => {
10465
- const file = path23.resolve(root, relativePath2);
10466
- if (!file.startsWith(root + path23.sep)) return null;
10467
- 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);
10468
10604
  return stat?.isFile() ? file : null;
10469
10605
  })
10470
10606
  );
@@ -10472,7 +10608,7 @@ var BlobIds = class {
10472
10608
  const fromGit = /* @__PURE__ */ new Map();
10473
10609
  const ids = forGit.length > 0 ? await this.hashObjects(
10474
10610
  root,
10475
- forGit.map((file) => path23.relative(root, file))
10611
+ forGit.map((file) => path24.relative(root, file))
10476
10612
  ) : null;
10477
10613
  if (ids) forGit.forEach((file, i) => fromGit.set(file, ids[i]));
10478
10614
  return Promise.all(
@@ -10481,7 +10617,7 @@ var BlobIds = class {
10481
10617
  const id = fromGit.get(file);
10482
10618
  if (id) return id;
10483
10619
  try {
10484
- return gitBlobId(await fs23.promises.readFile(file));
10620
+ return gitBlobId(await fs24.promises.readFile(file));
10485
10621
  } catch {
10486
10622
  return null;
10487
10623
  }
@@ -10678,7 +10814,7 @@ var CoverageCollection = class {
10678
10814
  }
10679
10815
  let timer;
10680
10816
  let finish;
10681
- const done = new Promise((resolve9) => {
10817
+ const done = new Promise((resolve11) => {
10682
10818
  let settled = false;
10683
10819
  finish = (server) => {
10684
10820
  if (settled) return;
@@ -10687,7 +10823,7 @@ var CoverageCollection = class {
10687
10823
  this.pending.delete(entry);
10688
10824
  void Promise.resolve(browser).then((files) => {
10689
10825
  this.assign(testCase, composeTestCoverage(server, files, this.options.settings));
10690
- resolve9();
10826
+ resolve11();
10691
10827
  });
10692
10828
  };
10693
10829
  });
@@ -10719,8 +10855,8 @@ var CoverageCollection = class {
10719
10855
  let timer;
10720
10856
  await Promise.race([
10721
10857
  Promise.all(waiting),
10722
- new Promise((resolve9) => {
10723
- timer = setTimeout(resolve9, boundMs);
10858
+ new Promise((resolve11) => {
10859
+ timer = setTimeout(resolve11, boundMs);
10724
10860
  })
10725
10861
  ]);
10726
10862
  clearTimeout(timer);
@@ -10838,10 +10974,13 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10838
10974
  this.streamManager = null;
10839
10975
  /** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
10840
10976
  this.setupSteps = [];
10841
- this.options = resolveOptions(rawOptions);
10977
+ const configDir = typeof rawOptions.configDir === "string" ? rawOptions.configDir : void 0;
10978
+ this.options = resolveOptions(rawOptions, configDir);
10842
10979
  this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
10843
10980
  this.listMode = isListMode();
10844
10981
  this.viaDesktopApp = usedDesktopDiscovery();
10982
+ this.desktopReplaced = desktopReplacedServer();
10983
+ this.viaDotEnv = usedDotEnv();
10845
10984
  this.coverageSettings = coverageSettings(this.options);
10846
10985
  const logger = new Logger(this.options.verbose ?? false);
10847
10986
  this.logger = logger;
@@ -10899,14 +11038,17 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10899
11038
  return;
10900
11039
  }
10901
11040
  if (this.viaDesktopApp) {
10902
- 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
+ );
10903
11044
  }
11045
+ if (this.viaDotEnv) this.logger.debug(`Read ${this.viaDotEnv.keys.join(", ")} from ${this.viaDotEnv.file}.`);
10904
11046
  this.startTime = (/* @__PURE__ */ new Date()).toISOString();
10905
11047
  this.testPathBase = configDirFromConfig(config);
10906
11048
  this.playwrightVersion = config.version;
10907
11049
  this.maxFailures = config.maxFailures ?? 0;
10908
11050
  if (this.options.captureResources !== false) {
10909
- 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`);
10910
11052
  process.env[PIWI_RESOURCES_RESULTS_ENV] = this.resourcesFile;
10911
11053
  try {
10912
11054
  this.sampler = new RunSampler({ outputDirs: (config.projects ?? []).map((project) => project.outputDir) });
@@ -11184,10 +11326,11 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11184
11326
  if (frames.length) testCase.testSourceFrames = frames;
11185
11327
  }
11186
11328
  if (this.options.collectPerformanceMetrics && result.steps?.length > 0) {
11187
- 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);
11188
11331
  const stepEvents = extractTestStepEvents(result.steps, result.startTime);
11189
11332
  const waitEvents = extractWaitEvents(result.steps);
11190
- const allEvents = [...stepEvents, ...waitEvents];
11333
+ const allEvents = [...stepEvents, ...pauseStepEvents(pauses), ...waitEvents];
11191
11334
  if (allEvents.length > 0) testCase.stepEvents = allEvents;
11192
11335
  }
11193
11336
  if (this.options.collectPerformanceMetrics && result.attachments) {
@@ -11275,7 +11418,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11275
11418
  if (!file.path || INTERNAL_ATTACHMENT_NAMES.has(file.name)) continue;
11276
11419
  try {
11277
11420
  const kind = artifactKind(file.name, file.contentType);
11278
- const bytes = fs24.statSync(file.path).size;
11421
+ const bytes = fs25.statSync(file.path).size;
11279
11422
  artifacts[kind] += bytes;
11280
11423
  this.artifactBytes[kind] += bytes;
11281
11424
  } catch {
@@ -11308,8 +11451,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11308
11451
  const censuses = [...this.resourceCensuses];
11309
11452
  if (this.resourcesFile) {
11310
11453
  try {
11311
- censuses.push(...parseResourceResults(fs24.readFileSync(this.resourcesFile, "utf8")));
11312
- fs24.rmSync(this.resourcesFile, { force: true });
11454
+ censuses.push(...parseResourceResults(fs25.readFileSync(this.resourcesFile, "utf8")));
11455
+ fs25.rmSync(this.resourcesFile, { force: true });
11313
11456
  } catch {
11314
11457
  }
11315
11458
  }
@@ -11534,7 +11677,7 @@ async function resolveSelection(options = {}) {
11534
11677
  }
11535
11678
 
11536
11679
  // src/internal/capture/capture-fixtures.ts
11537
- var path28 = __toESM(require("path"));
11680
+ var path30 = __toESM(require("path"));
11538
11681
  var import_node_zlib2 = require("zlib");
11539
11682
 
11540
11683
  // ../picker-dom/src/probe.ts
@@ -11993,8 +12136,8 @@ function installPickerOverlay(arg) {
11993
12136
  return root && root !== doc && root.host ? root.host : null;
11994
12137
  };
11995
12138
  const targetOf = (e) => {
11996
- const path32 = typeof e.composedPath === "function" ? e.composedPath() : null;
11997
- const first = path32 && path32.length ? path32[0] : null;
12139
+ const path34 = typeof e.composedPath === "function" ? e.composedPath() : null;
12140
+ const first = path34 && path34.length ? path34[0] : null;
11998
12141
  return first && first.nodeType === 1 ? first : e.target;
11999
12142
  };
12000
12143
  const buildChain = (raw) => {
@@ -12698,8 +12841,11 @@ function showPauseBar(arg) {
12698
12841
  };
12699
12842
  const buttons = [
12700
12843
  ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
12701
- ["Step", "step", "Pause again at the next action", false],
12844
+ ["Step", "step", "Pause again before the next call", false],
12702
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
+ ] : [],
12703
12849
  ["Finish", "finish", "Run on and pause no more in this test", false]
12704
12850
  ];
12705
12851
  for (const [label, choice, hint, primary] of buttons) {
@@ -13175,21 +13321,21 @@ var MAX_INDETERMINATE = 20;
13175
13321
  var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
13176
13322
  var LEVEL = `${STEP}(?:/${STEP})*`;
13177
13323
  var ELEMENT_PATH_RE = new RegExp(`^${LEVEL}(?:\\|${LEVEL})*$`);
13178
- function isElementPath(path32) {
13179
- 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);
13180
13326
  }
13181
13327
  var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
13182
13328
  function elementRef(v) {
13183
13329
  if (!v || typeof v !== "object") return null;
13184
- const { path: path32, label } = v;
13185
- if (!isElementPath(path32)) return null;
13186
- 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) : "" };
13187
13333
  }
13188
13334
  function textPoint(v) {
13189
13335
  if (!v || typeof v !== "object") return null;
13190
- const { path: path32, offset } = v;
13336
+ const { path: path34, offset } = v;
13191
13337
  const at = offsetOf(offset);
13192
- return isElementPath(path32) && at !== null ? { path: path32, offset: at } : null;
13338
+ return isElementPath(path34) && at !== null ? { path: path34, offset: at } : null;
13193
13339
  }
13194
13340
  function sanitizePageInteraction(raw) {
13195
13341
  if (!raw || typeof raw !== "object") return {};
@@ -13220,7 +13366,7 @@ function sanitizePageInteraction(raw) {
13220
13366
  }
13221
13367
 
13222
13368
  // src/internal/capture/locator-healing.ts
13223
- var path25 = __toESM(require("path"));
13369
+ var path26 = __toESM(require("path"));
13224
13370
 
13225
13371
  // ../core/src/locator-fingerprint.ts
13226
13372
  var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
@@ -13605,10 +13751,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
13605
13751
  }
13606
13752
  let rel = file;
13607
13753
  try {
13608
- rel = path25.relative(process.cwd(), file);
13754
+ rel = path26.relative(process.cwd(), file);
13609
13755
  } catch {
13610
13756
  }
13611
- rel = rel.split(path25.sep).join("/");
13757
+ rel = rel.split(path26.sep).join("/");
13612
13758
  if (rel.startsWith("./")) rel = rel.slice(2);
13613
13759
  return `${rel}:${m[3]}:${m[4]}`;
13614
13760
  }
@@ -13838,7 +13984,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
13838
13984
  }
13839
13985
 
13840
13986
  // src/internal/capture/pick-on-failure.ts
13841
- var path26 = __toESM(require("path"));
13987
+ var path27 = __toESM(require("path"));
13842
13988
  function parseLeafLocatorExpression(rawExpr) {
13843
13989
  const leaf = parseLeafLocatorCall(rawExpr);
13844
13990
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -13853,37 +13999,60 @@ ${err.stack ?? ""}`));
13853
13999
  const parsed2 = parseLeafLocatorExpression(header);
13854
14000
  if (!parsed2) continue;
13855
14001
  const loc = err.location;
13856
- 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;
13857
14003
  return { method: parsed2.method, args: parsed2.args, location };
13858
14004
  }
13859
14005
  return null;
13860
14006
  }
13861
- async function cleanupPicker(page) {
14007
+ var PICK_ENDED = "__piwiPickEnded";
14008
+ async function cleanupPicker(page, ended = false) {
13862
14009
  try {
13863
- await page.evaluate(() => {
13864
- const g = globalThis;
13865
- if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
13866
- if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
13867
- delete g.__piwiPickCleanup;
13868
- delete g.__piwiAnchorCleanup;
13869
- delete g.__piwiPickState;
13870
- delete g.__piwiPickChoice;
13871
- delete g.__piwiPickedElement;
13872
- delete g.__piwiAnchorState;
13873
- delete g.__piwiPickAnchors;
13874
- delete g.__piwiPickChainCount;
13875
- });
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
+ );
13876
14028
  } catch {
13877
14029
  }
13878
14030
  }
13879
- 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
+ };
13880
14051
  try {
13881
14052
  const overlayArg = { transport: "global", failing };
14053
+ await page.evaluate((endedKey) => delete globalThis[endedKey], PICK_ENDED);
13882
14054
  await page.evaluate(installPickerOverlay, overlayArg);
13883
- await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
13884
- timeout: 0,
13885
- polling: 250
13886
- });
14055
+ if (!await answered("__piwiPickState")) return null;
13887
14056
  const state = await page.evaluate(() => globalThis.__piwiPickState);
13888
14057
  if (state !== "picked") return null;
13889
14058
  const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
@@ -13904,10 +14073,7 @@ async function pickLocator(page, probe, failing) {
13904
14073
  leafLevel: level,
13905
14074
  leafTestId: attrs.attributes["data-testid"] ?? null
13906
14075
  });
13907
- await page.waitForFunction(() => globalThis.__piwiAnchorState !== void 0, void 0, {
13908
- timeout: 0,
13909
- polling: 250
13910
- });
14076
+ if (!await answered("__piwiAnchorState")) return null;
13911
14077
  const anchorResult = await page.evaluate(() => {
13912
14078
  const g = globalThis;
13913
14079
  return { state: g.__piwiAnchorState, anchors: g.__piwiPickAnchors ?? [], chainCount: g.__piwiPickChainCount };
@@ -13929,10 +14095,7 @@ async function pickLocator(page, probe, failing) {
13929
14095
  failing,
13930
14096
  choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
13931
14097
  });
13932
- await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
13933
- timeout: 0,
13934
- polling: 250
13935
- });
14098
+ if (!await answered("__piwiPickChoice")) return null;
13936
14099
  const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
13937
14100
  if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
13938
14101
  const picked = { ...ranked[choice], pickedByUser: true };
@@ -13951,7 +14114,7 @@ async function pickLocator(page, probe, failing) {
13951
14114
  ...anchors.length > 0 ? { anchors } : {}
13952
14115
  };
13953
14116
  } finally {
13954
- await cleanupPicker(page);
14117
+ await cleanupPicker(page, ended?.aborted);
13955
14118
  }
13956
14119
  }
13957
14120
  async function runLocatorPicker(page, testInfo, failed, probe) {
@@ -14006,7 +14169,7 @@ function applyPickToSnapshots(snapshots, pick) {
14006
14169
  }
14007
14170
 
14008
14171
  // src/internal/capture/pause-at.ts
14009
- var path27 = __toESM(require("path"));
14172
+ var path28 = __toESM(require("path"));
14010
14173
 
14011
14174
  // ../core/src/editor-send.ts
14012
14175
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
@@ -14047,18 +14210,32 @@ var PauseSet = class {
14047
14210
  return col > 0 && this.lines.has(callerLocation.slice(0, col));
14048
14211
  }
14049
14212
  };
14050
- var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
14213
+ var NO_BREAKPOINTS = new PauseSet(/* @__PURE__ */ new Set());
14051
14214
  function callSiteFile(file, cwd) {
14052
14215
  const slashed = file.replace(/\\/g, "/");
14053
- const absolute = path27.isAbsolute(file) || path27.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
14054
- let rel = absolute ? path27.relative(cwd, file) : path27.normalize(slashed);
14055
- 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, "/");
14056
14219
  if (rel.startsWith("./")) rel = rel.slice(2);
14057
14220
  return rel;
14058
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
+ }
14059
14236
  var ignoredLogged = /* @__PURE__ */ new Set();
14060
14237
  function parsePauseAt(value, cwd = process.cwd()) {
14061
- if (!value?.trim()) return EMPTY;
14238
+ if (!value?.trim()) return NO_BREAKPOINTS;
14062
14239
  const lines = /* @__PURE__ */ new Set();
14063
14240
  for (const raw of value.split(";")) {
14064
14241
  const entry = raw.trim();
@@ -14074,7 +14251,7 @@ function parsePauseAt(value, cwd = process.cwd()) {
14074
14251
  }
14075
14252
  lines.add(`${callSiteFile(m[1], cwd)}:${line}`);
14076
14253
  }
14077
- return lines.size ? new PauseSet(lines) : EMPTY;
14254
+ return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
14078
14255
  }
14079
14256
  var parsed = null;
14080
14257
  function currentPauseSet() {
@@ -14084,27 +14261,121 @@ function currentPauseSet() {
14084
14261
  }
14085
14262
  function pauseGateFromTestInfo(testInfo) {
14086
14263
  const use = testInfo.project?.use ?? {};
14087
- 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 };
14088
14266
  }
14089
14267
  function pauseSkipReason(gate) {
14090
14268
  if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
14091
14269
  if (gate.headless !== false)
14092
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`;
14093
14275
  return null;
14094
14276
  }
14095
- function rearmedTimeout(before, pausedMs) {
14096
- 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;
14097
14374
  }
14098
14375
  function pausePlace(callerLocation) {
14099
- const col = callerLocation.lastIndexOf(":");
14100
- const fileLine = col > 0 ? callerLocation.slice(0, col) : callerLocation;
14376
+ const fileLine = callSiteLine(callerLocation);
14101
14377
  return fileLine.slice(fileLine.lastIndexOf("/") + 1);
14102
14378
  }
14103
- function assertionLabel(expression, isNot) {
14104
- const [first = "", ...rest] = expression.split(".").filter(Boolean);
14105
- const name = first + rest.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
14106
- return `${isNot ? "not." : ""}${name || "expect"}`;
14107
- }
14108
14379
  function pickPlace(callerLocation) {
14109
14380
  const m = /^(.+):(\d+):\d+$/.exec(callerLocation);
14110
14381
  if (!m) return null;
@@ -14149,59 +14420,312 @@ function logPauseSkipOnce(reason) {
14149
14420
  skipLogged = true;
14150
14421
  console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt} is set but breakpoints are skipped: ${reason}`);
14151
14422
  }
14423
+ var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
14424
+ var MAX_NAVIGATED = 10;
14152
14425
  var HIGHLIGHT_MS = 2e3;
14153
- async function pauseHere(page, testInfo, point, probe, onPick) {
14154
- const before = testInfo.timeout;
14155
- const started = Date.now();
14426
+ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
14156
14427
  let shown = null;
14157
14428
  try {
14158
- testInfo.setTimeout(0);
14159
14429
  const highlight = point.target?.highlight;
14160
14430
  if (typeof highlight === "function") {
14161
14431
  let timer;
14162
14432
  shown = await Promise.race([
14163
14433
  Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
14164
- new Promise((resolve9) => {
14165
- timer = setTimeout(() => resolve9(null), HIGHLIGHT_MS);
14434
+ new Promise((resolve11) => {
14435
+ timer = setTimeout(() => resolve11(null), HIGHLIGHT_MS);
14166
14436
  })
14167
14437
  ]);
14168
14438
  clearTimeout(timer);
14169
14439
  }
14170
14440
  const arg = {
14171
14441
  place: pausePlace(point.location),
14172
- attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
14173
- action: point.action,
14174
- locator: point.locator
14442
+ attempt: point.attempt,
14443
+ action: point.call,
14444
+ locator: null,
14445
+ record: !!onRecord
14175
14446
  };
14176
14447
  console.log(
14177
- `[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).`
14178
14449
  );
14450
+ let navigated = 0;
14179
14451
  for (; ; ) {
14180
- await page.evaluate(showPauseBar, arg);
14181
- await page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 });
14182
- const choice = await page.evaluate(takePauseState);
14183
- if (choice === "resume" || choice === "step" || choice === "finish") return choice;
14184
- if (choice !== "pick") continue;
14185
- const picked = await pickLocator(page, probe, null).catch(() => null);
14186
- 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
+ }
14187
14474
  }
14188
14475
  } catch {
14189
14476
  return "resume";
14190
14477
  } finally {
14191
14478
  try {
14479
+ if (ended?.aborted) await page.evaluate(takePauseState);
14192
14480
  const dispose = shown?.dispose;
14193
14481
  const hide = page.hideHighlight;
14194
14482
  if (typeof dispose === "function") await dispose.call(shown);
14195
14483
  else if (typeof hide === "function") await hide.call(page);
14196
14484
  } catch {
14197
14485
  }
14198
- try {
14199
- testInfo.setTimeout(rearmedTimeout(before, Date.now() - started));
14200
- } catch {
14201
- }
14202
14486
  }
14203
14487
  }
14204
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
+
14205
14729
  // src/internal/capture/page-interaction.ts
14206
14730
  function readPageInteraction() {
14207
14731
  const doc = globalThis.document;
@@ -14497,6 +15021,13 @@ function createSink() {
14497
15021
  pickOffered: false,
14498
15022
  userPick: null,
14499
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,
14500
15031
  locatorPages: new LocatorPageLog(),
14501
15032
  codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
14502
15033
  serverCoverage: /* @__PURE__ */ new Map(),
@@ -14522,7 +15053,7 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
14522
15053
  async function startCodeReachOnOpenPages() {
14523
15054
  if (!browserCoverageInWorker() || OPEN_PAGES.size === 0) return;
14524
15055
  const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
14525
- 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())]);
14526
15057
  }
14527
15058
  function configuredCodeReachRoots() {
14528
15059
  const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
@@ -14536,7 +15067,7 @@ function configuredCodeReachRoots() {
14536
15067
  }
14537
15068
  } catch {
14538
15069
  }
14539
- return raw.split(path28.delimiter).filter(Boolean);
15070
+ return raw.split(path30.delimiter).filter(Boolean);
14540
15071
  }
14541
15072
  async function stopSinkCodeReach(sink, closing) {
14542
15073
  const stopping = [];
@@ -14585,8 +15116,8 @@ async function drainPendingProbes(capMs) {
14585
15116
  let cap;
14586
15117
  await Promise.race([
14587
15118
  Promise.allSettled(PENDING_PROBES),
14588
- new Promise((resolve9) => {
14589
- cap = setTimeout(resolve9, capMs);
15119
+ new Promise((resolve11) => {
15120
+ cap = setTimeout(resolve11, capMs);
14590
15121
  })
14591
15122
  ]);
14592
15123
  clearTimeout(cap);
@@ -14626,10 +15157,10 @@ async function readWebVitals(page) {
14626
15157
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
14627
15158
  paint[key] = Math.round(entry.startTime);
14628
15159
  }
14629
- const readBuffered = (type, extra) => new Promise((resolve9) => {
15160
+ const readBuffered = (type, extra) => new Promise((resolve11) => {
14630
15161
  try {
14631
15162
  const PO = globalThis.PerformanceObserver;
14632
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve9(null);
15163
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve11(null);
14633
15164
  const out = [];
14634
15165
  const po = new PO((list) => out.push(...list.getEntries()));
14635
15166
  po.observe({ type, buffered: true, ...extra });
@@ -14639,10 +15170,10 @@ async function readWebVitals(page) {
14639
15170
  po.disconnect();
14640
15171
  } catch {
14641
15172
  }
14642
- resolve9(out);
15173
+ resolve11(out);
14643
15174
  }, 0);
14644
15175
  } catch {
14645
- resolve9(null);
15176
+ resolve11(null);
14646
15177
  }
14647
15178
  });
14648
15179
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -14757,12 +15288,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
14757
15288
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
14758
15289
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
14759
15290
  function withTimeout(promise, ms) {
14760
- return new Promise((resolve9, reject) => {
15291
+ return new Promise((resolve11, reject) => {
14761
15292
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
14762
15293
  promise.then(
14763
15294
  (value) => {
14764
15295
  clearTimeout(timer);
14765
- resolve9(value);
15296
+ resolve11(value);
14766
15297
  },
14767
15298
  (error) => {
14768
15299
  clearTimeout(timer);
@@ -14877,6 +15408,7 @@ async function maybeOpenPicker(sink, closing) {
14877
15408
  var INSTRUMENTED_PAGES = /* @__PURE__ */ new WeakSet();
14878
15409
  var OPEN_PAGES = /* @__PURE__ */ new Set();
14879
15410
  var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
15411
+ var OPEN_CONTEXTS = /* @__PURE__ */ new Set();
14880
15412
  var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
14881
15413
  var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
14882
15414
  var ACTION_METHOD_SET = new Set(ACTION_METHODS);
@@ -14997,8 +15529,20 @@ function recordLocatorPage(sink, page, locator, location, arrival) {
14997
15529
  } catch {
14998
15530
  }
14999
15531
  }
15000
- function pausesAt(pause, callerLocation) {
15001
- 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);
15002
15546
  }
15003
15547
  function locatorSource(target, method, args) {
15004
15548
  try {
@@ -15008,41 +15552,219 @@ function locatorSource(target, method, args) {
15008
15552
  }
15009
15553
  return renderFailing({ method, args });
15010
15554
  }
15011
- async function pauseBefore(sink, page, target, callerLocation, action, origin) {
15012
- 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) {
15013
15602
  const testInfo = sink.testInfo;
15014
- if (!pause || !testInfo || !callerLocation) return;
15015
- pause.next = false;
15016
- try {
15017
- const locator = locatorSource(target, origin.method, origin.args);
15018
- const onPick = async (picked) => {
15019
- const pick = {
15020
- 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 },
15021
15613
  ...picked
15022
- };
15023
- applyPickToSnapshots(sink.capturedLocators, pick);
15024
- sink.probedLocations.add(callerLocation);
15025
- const place = callerLocation.slice(0, callerLocation.lastIndexOf(":"));
15026
- console.log(`[piwi] Locator picked at ${place}: ${picked.picked.locator}`);
15027
- await postPickToEditor(
15028
- process.env[PIWI_LOCAL_DEBUG_ENV.editorSend],
15029
- pickPayload(picked.picked.locator, callerLocation)
15030
- );
15031
- };
15032
- 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(
15033
15626
  page,
15034
15627
  () => pauseHere(
15035
15628
  page,
15036
- testInfo,
15037
- { 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
+ },
15038
15635
  { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
15039
- onPick
15636
+ onPick,
15637
+ recorderAt(page, testInfo, location, reason === "page-pause" ? "pause" : "breakpoint"),
15638
+ end.signal
15040
15639
  )
15041
- );
15042
- if (choice === "step") pause.next = true;
15043
- else if (choice === "finish") pause.finished = true;
15044
- } 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)));
15690
+ }
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();
15045
15766
  }
15767
+ return (options) => own.call(page, options);
15046
15768
  }
15047
15769
  function wrapLocator(page, locator, originMethod, originArgs) {
15048
15770
  return new Proxy(locator, {
@@ -15065,36 +15787,35 @@ function wrapLocator(page, locator, originMethod, originArgs) {
15065
15787
  const isNot = Boolean(callArgs[1]?.isNot);
15066
15788
  const callerLocation = sink ? captureCallerLocation() : null;
15067
15789
  if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
15068
- if (sink?.pause && pausesAt(sink.pause, callerLocation)) {
15069
- 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 = {
15070
15797
  method: originMethod,
15071
- args: originArgs
15072
- });
15073
- }
15074
- if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
15075
- return callMethod(target, prop, callArgs);
15076
- }
15077
- sink.lastActivePage = page;
15078
- const used = {
15079
- method: originMethod,
15080
- args: originArgs,
15081
- raw: `${originMethod}(${JSON.stringify(originArgs)})`
15082
- };
15083
- const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
15084
- let seq = -1;
15085
- if (!alreadyProbed) {
15086
- seq = sink.capturedLocators.length;
15087
- sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
15088
- }
15089
- const result = await callMethod(target, prop, callArgs);
15090
- const matches = result?.matches;
15091
- if (matches === true && !alreadyProbed) {
15092
- if (callerLocation) sink.probedLocations.add(callerLocation);
15093
- startElementCapture(sink, page, target, seq, callerLocation, used);
15094
- } else if (matches === false) {
15095
- 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);
15096
15818
  }
15097
- return result;
15098
15819
  };
15099
15820
  }
15100
15821
  if (!ACTION_METHOD_SET.has(prop)) return original;
@@ -15109,9 +15830,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
15109
15830
  args: originArgs,
15110
15831
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
15111
15832
  };
15112
- if (sink.pause && pausesAt(sink.pause, callerLocation)) {
15113
- await pauseBefore(sink, page, target, callerLocation, String(prop), used);
15114
- }
15833
+ const call = knowLocatorCall(sink, page, target, callerLocation, used);
15115
15834
  const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
15116
15835
  let seq = -1;
15117
15836
  if (!alreadyProbed) {
@@ -15129,8 +15848,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
15129
15848
  } catch (error) {
15130
15849
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
15131
15850
  throw error;
15851
+ } finally {
15852
+ forgetLocatorCall(sink, call);
15132
15853
  }
15133
- if (!alreadyProbed) {
15854
+ if (!alreadyProbed && !call?.picked) {
15134
15855
  if (callerLocation) sink.probedLocations.add(callerLocation);
15135
15856
  startElementCapture(sink, page, target, seq, callerLocation, used);
15136
15857
  }
@@ -15183,12 +15904,17 @@ function instrumentPage(page) {
15183
15904
  }
15184
15905
  const ctx = pageContext(page);
15185
15906
  if (ctx) instrumentContext(ctx);
15907
+ const playwrightPause = playwrightPauseOf(page);
15908
+ if (playwrightPause) {
15909
+ page.pause = () => pauseInPlace(page, playwrightPause, captureCallerLocation());
15910
+ }
15186
15911
  if (typeof page.close === "function") {
15187
15912
  const originalClose = page.close.bind(page);
15188
15913
  page.close = async (...args) => {
15189
15914
  await drainPendingProbes(1e3);
15190
15915
  const sink = currentSink;
15191
15916
  if (sink) {
15917
+ await endShownStops(sink, "closed", { page });
15192
15918
  await stashPageState(sink, { page });
15193
15919
  await maybeOpenPicker(sink, { page });
15194
15920
  await stopSinkCodeReach(sink, { page });
@@ -15277,8 +16003,18 @@ function instrumentPage(page) {
15277
16003
  } catch {
15278
16004
  }
15279
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
+ });
15280
16012
  const onRequestEnd = (request3, failed) => {
15281
16013
  const sink = currentSink;
16014
+ if (sink?.releasedRequests.delete(request3)) {
16015
+ sink.releasedQuietSince = Date.now();
16016
+ if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
16017
+ }
15282
16018
  if (!sink || RECORDED_OPEN.has(request3)) return;
15283
16019
  sink.lastActivePage = page;
15284
16020
  const response = sink.openResponses.get(request3) ?? null;
@@ -15384,6 +16120,10 @@ async function awaitFlakeConditions() {
15384
16120
  function instrumentContext(context) {
15385
16121
  if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
15386
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
+ }
15387
16127
  observeOwnApiRequests(context);
15388
16128
  if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
15389
16129
  void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
@@ -15403,6 +16143,7 @@ function instrumentContext(context) {
15403
16143
  await drainPendingProbes(1e3);
15404
16144
  const sink = currentSink;
15405
16145
  if (sink) {
16146
+ await endShownStops(sink, "closed", { context });
15406
16147
  await stashPageState(sink, { context });
15407
16148
  await maybeOpenPicker(sink, { context });
15408
16149
  await stopSinkCodeReach(sink, { context });
@@ -15419,6 +16160,7 @@ function patchBrowser(browser) {
15419
16160
  browser.newPage = async (...args) => {
15420
16161
  const page = await originalNewPage(...args);
15421
16162
  instrumentPage(page);
16163
+ await armBreakpoints([pageContext(page)]);
15422
16164
  await awaitFlakeConditions();
15423
16165
  await maybeStartCodeReach(page);
15424
16166
  return page;
@@ -15427,6 +16169,7 @@ function patchBrowser(browser) {
15427
16169
  browser.newContext = async (...args) => {
15428
16170
  const context = await originalNewContext(...args);
15429
16171
  instrumentContext(context);
16172
+ await armBreakpoints([context]);
15430
16173
  return context;
15431
16174
  };
15432
16175
  if (typeof browser.close === "function") {
@@ -15478,14 +16221,20 @@ async function flushSink(sink, testInfo) {
15478
16221
  let drainDeadline;
15479
16222
  await Promise.race([
15480
16223
  Promise.allSettled(sink.capturePromises),
15481
- new Promise((resolve9) => {
15482
- drainDeadline = setTimeout(resolve9, 2e3);
16224
+ new Promise((resolve11) => {
16225
+ drainDeadline = setTimeout(resolve11, 2e3);
15483
16226
  })
15484
16227
  ]);
15485
16228
  clearTimeout(drainDeadline);
15486
16229
  const page = sink.lastActivePage;
15487
16230
  const pageReadable = page !== null && !isPageClosed(page);
15488
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
+ }
15489
16238
  if (sink.capturedLocators.length > 0) {
15490
16239
  await testInfo.attach(ATTACHMENT_NAMES.locators, {
15491
16240
  contentType: "application/json",
@@ -15686,6 +16435,7 @@ var piwiFixtures = {
15686
16435
  // never double-wraps a page the browser patch already instrumented.
15687
16436
  page: async ({ page }, use) => {
15688
16437
  instrumentPage(page);
16438
+ await armBreakpoints([pageContext(page)]);
15689
16439
  await awaitFlakeConditions();
15690
16440
  await maybeStartCodeReach(page);
15691
16441
  await use(page);
@@ -15706,8 +16456,9 @@ var piwiFixtures = {
15706
16456
  if (pauseSet.size) {
15707
16457
  const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
15708
16458
  if (reason) logPauseSkipOnce(reason);
15709
- else sink.pause = { set: pauseSet, next: false, finished: false };
16459
+ else sink.pause = { set: pauseSet, step: false, finished: false };
15710
16460
  }
16461
+ if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
15711
16462
  if (isFlakeMode()) {
15712
16463
  const plan = loadFlakePlan();
15713
16464
  const role = flakeRoleForTest(plan, {
@@ -15732,9 +16483,12 @@ var piwiFixtures = {
15732
16483
  activeResourceLedger()?.testStarted(testInfo);
15733
16484
  currentSink = sink;
15734
16485
  try {
16486
+ await armBreakpoints(OPEN_CONTEXTS);
15735
16487
  await startCodeReachOnOpenPages();
15736
16488
  await use();
15737
16489
  } finally {
16490
+ await endShownStops(sink, "ended");
16491
+ await awaitReleasedCalls(sink);
15738
16492
  currentSink = null;
15739
16493
  if (sink.probeItem) {
15740
16494
  const applied = sink.probeInterception?.applied() ?? false;
@@ -15792,13 +16546,13 @@ function extendPiwiFixtures(test) {
15792
16546
  }
15793
16547
 
15794
16548
  // src/internal/ai/ai-fixtures.ts
15795
- var fs26 = __toESM(require("fs"));
15796
- var path31 = __toESM(require("path"));
16549
+ var fs28 = __toESM(require("fs"));
16550
+ var path33 = __toESM(require("path"));
15797
16551
  var import_test = require("@playwright/test");
15798
16552
 
15799
16553
  // src/internal/ai/artifact.ts
15800
- var fs25 = __toESM(require("fs"));
15801
- var path29 = __toESM(require("path"));
16554
+ var fs27 = __toESM(require("fs"));
16555
+ var path31 = __toESM(require("path"));
15802
16556
  var ARTIFACT_VERSION = 1;
15803
16557
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
15804
16558
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -15891,7 +16645,7 @@ function parseEntry(text) {
15891
16645
  function readEntry(file) {
15892
16646
  let text;
15893
16647
  try {
15894
- text = fs25.readFileSync(file, "utf8");
16648
+ text = fs27.readFileSync(file, "utf8");
15895
16649
  } catch (error) {
15896
16650
  if (error.code === "ENOENT") return null;
15897
16651
  throw error;
@@ -15901,28 +16655,28 @@ function readEntry(file) {
15901
16655
  function writeEntry(file, entry) {
15902
16656
  const canonical = serializeEntry(entry);
15903
16657
  return withEntryLock(file, () => {
15904
- if (fs25.existsSync(file) && fs25.readFileSync(file, "utf8") === canonical) return { written: false };
15905
- 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 });
15906
16660
  const tmp = `${file}.${process.pid}.tmp`;
15907
- fs25.writeFileSync(tmp, canonical);
15908
- fs25.renameSync(tmp, file);
16661
+ fs27.writeFileSync(tmp, canonical);
16662
+ fs27.renameSync(tmp, file);
15909
16663
  return { written: true };
15910
16664
  });
15911
16665
  }
15912
16666
  function withEntryLock(file, fn) {
15913
16667
  const lock = `${file}.lock`;
15914
- fs25.mkdirSync(path29.dirname(file), { recursive: true });
16668
+ fs27.mkdirSync(path31.dirname(file), { recursive: true });
15915
16669
  const deadline = Date.now() + 5e3;
15916
16670
  for (; ; ) {
15917
16671
  try {
15918
- const fd = fs25.openSync(lock, "wx");
15919
- fs25.closeSync(fd);
16672
+ const fd = fs27.openSync(lock, "wx");
16673
+ fs27.closeSync(fd);
15920
16674
  break;
15921
16675
  } catch (error) {
15922
16676
  if (error.code !== "EEXIST") throw error;
15923
16677
  if (Date.now() > deadline) {
15924
16678
  try {
15925
- fs25.unlinkSync(lock);
16679
+ fs27.unlinkSync(lock);
15926
16680
  } catch {
15927
16681
  }
15928
16682
  }
@@ -15932,7 +16686,7 @@ function withEntryLock(file, fn) {
15932
16686
  return fn();
15933
16687
  } finally {
15934
16688
  try {
15935
- fs25.unlinkSync(lock);
16689
+ fs27.unlinkSync(lock);
15936
16690
  } catch {
15937
16691
  }
15938
16692
  }
@@ -15940,7 +16694,7 @@ function withEntryLock(file, fn) {
15940
16694
 
15941
16695
  // src/internal/ai/keys.ts
15942
16696
  var crypto7 = __toESM(require("crypto"));
15943
- var path30 = __toESM(require("path"));
16697
+ var path32 = __toESM(require("path"));
15944
16698
  var DEFAULT_AI_DIR = "__piwi__";
15945
16699
  function normalizeTemplate(template) {
15946
16700
  return template.trim().replace(/\s+/g, " ").toLowerCase();
@@ -15954,13 +16708,13 @@ function slug(text, maxLength = 40) {
15954
16708
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
15955
16709
  }
15956
16710
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
15957
- return path30.join(path30.dirname(specFile), dir, path30.basename(specFile));
16711
+ return path32.join(path32.dirname(specFile), dir, path32.basename(specFile));
15958
16712
  }
15959
16713
  function entryPath(params) {
15960
16714
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
15961
16715
  const hash = hashTemplate(basis, params.ordinal ?? 0);
15962
16716
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
15963
- return path30.join(entryDir(params.specFile, params.dir), name);
16717
+ return path32.join(entryDir(params.specFile, params.dir), name);
15964
16718
  }
15965
16719
  function parseLocation2(location) {
15966
16720
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -16563,7 +17317,7 @@ function testIdentity(testInfo) {
16563
17317
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
16564
17318
  }
16565
17319
  function relativeToCwd(file) {
16566
- return path31.relative(process.cwd(), file).split(path31.sep).join("/");
17320
+ return path33.relative(process.cwd(), file).split(path33.sep).join("/");
16567
17321
  }
16568
17322
  function recordIntents(intents, entry) {
16569
17323
  const add = (locator, kind) => {
@@ -16590,7 +17344,7 @@ function createAiApi(page, testInfo, config, used, intents) {
16590
17344
  const readSource = () => {
16591
17345
  if (source === null) {
16592
17346
  try {
16593
- source = fs26.readFileSync(testInfo.file, "utf8");
17347
+ source = fs28.readFileSync(testInfo.file, "utf8");
16594
17348
  } catch {
16595
17349
  source = "";
16596
17350
  }