@piwitests/reporter 0.59.0 → 0.61.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",
@@ -128,6 +203,7 @@ var PIWI_ENV_KEYS = {
128
203
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
129
204
  coverageBrowser: "PIWI_COVERAGE_BROWSER",
130
205
  coverageServer: "PIWI_COVERAGE_SERVER",
206
+ coverageCss: "PIWI_COVERAGE_CSS",
131
207
  externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
132
208
  externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
133
209
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
@@ -215,11 +291,27 @@ var ENV_FALLBACK_SPECS = [
215
291
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
216
292
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
217
293
  ];
294
+ var DOTENV_OPTIONS = ["serverUrl", "apiKey", "projectName"];
295
+ var DESKTOP_APP_MODES = ["prefer", "fallback", "off"];
296
+ function desktopAppMode(option, envValue) {
297
+ const fromEnv = envValue?.trim().toLowerCase();
298
+ if (DESKTOP_APP_MODES.includes(fromEnv)) return fromEnv;
299
+ if (DESKTOP_APP_MODES.includes(option)) return option;
300
+ return "fallback";
301
+ }
218
302
  var desktopDiscovered = false;
303
+ var desktopReplacedServerUrl = null;
304
+ var dotEnvUse = null;
219
305
  function usedDesktopDiscovery() {
220
306
  return desktopDiscovered;
221
307
  }
222
- function resolveOptions(raw) {
308
+ function desktopReplacedServer() {
309
+ return desktopReplacedServerUrl;
310
+ }
311
+ function usedDotEnv() {
312
+ return dotEnvUse;
313
+ }
314
+ function resolveOptions(raw, configDir) {
223
315
  const env = process.env;
224
316
  const mergedRaw = { ...raw };
225
317
  for (const spec of ENV_FALLBACK_SPECS) {
@@ -231,24 +323,49 @@ function resolveOptions(raw) {
231
323
  mergedRaw[spec.option] = spec.kind === "number" ? Number(value) : value;
232
324
  }
233
325
  }
326
+ dotEnvUse = null;
327
+ if (configDir) {
328
+ const file = path2.join(configDir, ".env");
329
+ const dotEnv = readDotEnv(file);
330
+ const keys = [];
331
+ for (const option of DOTENV_OPTIONS) {
332
+ const value = dotEnv[PIWI_ENV_KEYS[option]];
333
+ if (mergedRaw[option] !== void 0 || !value) continue;
334
+ mergedRaw[option] = value;
335
+ keys.push(PIWI_ENV_KEYS[option]);
336
+ }
337
+ if (keys.length > 0) dotEnvUse = { file, keys };
338
+ }
234
339
  desktopDiscovered = false;
340
+ desktopReplacedServerUrl = null;
341
+ const mode = desktopAppMode(mergedRaw.desktopApp, env[PIWI_ENV_KEYS.desktopApp]);
342
+ mergedRaw.desktopApp = mode;
235
343
  const serverUrlUnset = mergedRaw.serverUrl === void 0;
236
344
  const apiKeyUnset = mergedRaw.apiKey === void 0 || mergedRaw.apiKey === null;
237
- if (serverUrlUnset && apiKeyUnset) {
345
+ const nothingConfigured = serverUrlUnset && apiKeyUnset;
346
+ const preferLocally = mode === "prefer" && !detectCi(env);
347
+ if (mode !== "off" && (nothingConfigured || preferLocally)) {
238
348
  const desktop = readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath());
239
349
  if (desktop) {
350
+ if (!serverUrlUnset && mergedRaw.serverUrl !== desktop.url) desktopReplacedServerUrl = mergedRaw.serverUrl;
240
351
  mergedRaw.serverUrl = desktop.url;
241
352
  mergedRaw.apiKey = desktop.token;
353
+ mergedRaw.username = null;
354
+ mergedRaw.password = null;
242
355
  desktopDiscovered = true;
243
356
  }
244
357
  }
245
358
  const coverage = { ...mergedRaw.coverage };
246
359
  const coverageBrowser = env[PIWI_ENV_KEYS.coverageBrowser];
247
360
  const coverageServer = env[PIWI_ENV_KEYS.coverageServer];
361
+ const coverageCss = env[PIWI_ENV_KEYS.coverageCss];
248
362
  if (coverage.browser === void 0 && coverageBrowser)
249
363
  coverage.browser = coverageBrowser;
250
364
  if (coverage.server === void 0 && coverageServer) coverage.server = coverageServer;
251
- if (coverage.browser !== void 0 || coverage.server !== void 0) mergedRaw.coverage = coverage;
365
+ if (coverage.css === void 0 && coverageCss) coverage.css = coverageCss;
366
+ if (coverage.browser !== void 0 || coverage.server !== void 0 || coverage.css !== void 0) {
367
+ mergedRaw.coverage = coverage;
368
+ }
252
369
  const externalId = { ...mergedRaw.externalId };
253
370
  const externalIdPattern = env[PIWI_ENV_KEYS.externalIdPattern];
254
371
  const externalIdOnDuplicate = env[PIWI_ENV_KEYS.externalIdOnDuplicate];
@@ -263,6 +380,8 @@ function resolveOptions(raw) {
263
380
  function applyOptionsToEnv(options) {
264
381
  const env = process.env;
265
382
  if (options.serverUrl !== void 0) env[PIWI_ENV_KEYS.serverUrl] = options.serverUrl;
383
+ if (options.desktopApp !== void 0 && !env[PIWI_ENV_KEYS.desktopApp])
384
+ env[PIWI_ENV_KEYS.desktopApp] = options.desktopApp;
266
385
  if (options.projectName !== void 0) env[PIWI_ENV_KEYS.projectName] = options.projectName;
267
386
  if (options.verbose !== void 0) env[PIWI_ENV_KEYS.verbose] = String(options.verbose);
268
387
  if (options.apiKey) env[PIWI_ENV_KEYS.apiKey] = options.apiKey;
@@ -292,9 +411,11 @@ function applyOptionsToEnv(options) {
292
411
  if (options.collectPerformanceMetrics === false) {
293
412
  env[PIWI_ENV_KEYS.coverageBrowser] = "off";
294
413
  env[PIWI_ENV_KEYS.coverageServer] = "off";
414
+ env[PIWI_ENV_KEYS.coverageCss] = "off";
295
415
  } else {
296
416
  if (options.coverage?.browser !== void 0) env[PIWI_ENV_KEYS.coverageBrowser] = options.coverage.browser;
297
417
  if (options.coverage?.server !== void 0) env[PIWI_ENV_KEYS.coverageServer] = options.coverage.server;
418
+ if (options.coverage?.css !== void 0) env[PIWI_ENV_KEYS.coverageCss] = options.coverage.css;
298
419
  }
299
420
  if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
300
421
  env[PIWI_ENV_KEYS.captureServerTraces] = "false";
@@ -545,7 +666,7 @@ var HttpClient = class {
545
666
  }
546
667
  /** Unified request core: transport, headers, auth, response accumulation, timeout. */
547
668
  request(method, pathname, opts) {
548
- return new Promise((resolve9, reject) => {
669
+ return new Promise((resolve11, reject) => {
549
670
  if (this.closedReason !== null) {
550
671
  reject(new Error(this.closedReason));
551
672
  return;
@@ -568,7 +689,7 @@ var HttpClient = class {
568
689
  data += chunk;
569
690
  });
570
691
  res.on("end", () => {
571
- resolve9({ status: res.statusCode ?? 0, text: data, headers: res.headers });
692
+ resolve11({ status: res.statusCode ?? 0, text: data, headers: res.headers });
572
693
  });
573
694
  res.on("error", reject);
574
695
  res.on("close", () => {
@@ -607,8 +728,8 @@ var HttpClient = class {
607
728
  };
608
729
 
609
730
  // src/internal/submit/uploader.ts
610
- var fs2 = __toESM(require("fs"));
611
- var path2 = __toESM(require("path"));
731
+ var fs3 = __toESM(require("fs"));
732
+ var path3 = __toESM(require("path"));
612
733
  var import_form_data2 = __toESM(require("form-data"));
613
734
 
614
735
  // src/internal/submit/serializer.ts
@@ -858,8 +979,8 @@ var Uploader = class {
858
979
  if (traceInfo) {
859
980
  form.append("trace_hash", traceInfo.hash);
860
981
  if (includeTraceFile2) {
861
- form.append("trace", fs2.createReadStream(traceInfo.tracePath), {
862
- filename: path2.basename(traceInfo.tracePath)
982
+ form.append("trace", fs3.createReadStream(traceInfo.tracePath), {
983
+ filename: path3.basename(traceInfo.tracePath)
863
984
  });
864
985
  }
865
986
  }
@@ -871,7 +992,7 @@ var Uploader = class {
871
992
  )
872
993
  );
873
994
  for (const a of attachments) {
874
- form.append("attach_file", fs2.createReadStream(a.path), { filename: a.originalName });
995
+ form.append("attach_file", fs3.createReadStream(a.path), { filename: a.originalName });
875
996
  }
876
997
  }
877
998
  return form;
@@ -924,7 +1045,7 @@ var Uploader = class {
924
1045
  )
925
1046
  );
926
1047
  for (const a of attachments) {
927
- form.append(`attach_file_${i}`, fs2.createReadStream(a.path), { filename: a.originalName });
1048
+ form.append(`attach_file_${i}`, fs3.createReadStream(a.path), { filename: a.originalName });
928
1049
  attachmentCount++;
929
1050
  }
930
1051
  }
@@ -933,8 +1054,8 @@ var Uploader = class {
933
1054
  let traceCount = 0;
934
1055
  for (const [i, tc] of testCases.entries()) {
935
1056
  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) });
1057
+ if (fs3.existsSync(tp)) {
1058
+ form.append(`trace_${i}`, fs3.createReadStream(tp), { filename: path3.basename(tp) });
938
1059
  traceCount++;
939
1060
  }
940
1061
  }
@@ -944,9 +1065,9 @@ var Uploader = class {
944
1065
  };
945
1066
 
946
1067
  // src/internal/streaming/stream-buffer.ts
947
- var path3 = __toESM(require("path"));
1068
+ var path4 = __toESM(require("path"));
948
1069
  var os3 = __toESM(require("os"));
949
- var fs3 = __toESM(require("fs"));
1070
+ var fs4 = __toESM(require("fs"));
950
1071
 
951
1072
  // src/internal/support/instance-id.ts
952
1073
  var crypto = __toESM(require("crypto"));
@@ -967,14 +1088,14 @@ var StreamBuffer = class {
967
1088
  }
968
1089
  /** Scope the buffer to a run. Until then the buffer is empty and appends are dropped. */
969
1090
  bindRun(runId) {
970
- this.filePath = path3.join(os3.tmpdir(), `${this.prefix}-${runId}.jsonl`);
1091
+ this.filePath = path4.join(os3.tmpdir(), `${this.prefix}-${runId}.jsonl`);
971
1092
  }
972
1093
  /** Append one or more events to the on-disk buffer */
973
1094
  append(events) {
974
1095
  if (!this.filePath || events.length === 0) return;
975
1096
  try {
976
1097
  const lines = events.map((e) => JSON.stringify(e) + "\n").join("");
977
- fs3.appendFileSync(this.filePath, lines, "utf8");
1098
+ fs4.appendFileSync(this.filePath, lines, "utf8");
978
1099
  } catch {
979
1100
  }
980
1101
  }
@@ -982,8 +1103,8 @@ var StreamBuffer = class {
982
1103
  load() {
983
1104
  if (!this.filePath) return [];
984
1105
  try {
985
- if (fs3.existsSync(this.filePath)) {
986
- const content = fs3.readFileSync(this.filePath, "utf8");
1106
+ if (fs4.existsSync(this.filePath)) {
1107
+ const content = fs4.readFileSync(this.filePath, "utf8");
987
1108
  return content.split("\n").filter(Boolean).map((line) => JSON.parse(line));
988
1109
  }
989
1110
  } catch {
@@ -994,7 +1115,7 @@ var StreamBuffer = class {
994
1115
  clear() {
995
1116
  if (!this.filePath) return;
996
1117
  try {
997
- if (fs3.existsSync(this.filePath)) fs3.unlinkSync(this.filePath);
1118
+ if (fs4.existsSync(this.filePath)) fs4.unlinkSync(this.filePath);
998
1119
  } catch {
999
1120
  }
1000
1121
  }
@@ -1003,14 +1124,14 @@ var StreamBuffer = class {
1003
1124
  const dir = os3.tmpdir();
1004
1125
  let names;
1005
1126
  try {
1006
- names = fs3.readdirSync(dir).filter((name) => name.startsWith(this.prefix));
1127
+ names = fs4.readdirSync(dir).filter((name) => name.startsWith(this.prefix));
1007
1128
  } catch {
1008
1129
  return;
1009
1130
  }
1010
1131
  for (const name of names) {
1011
1132
  try {
1012
- const file = path3.join(dir, name);
1013
- if (Date.now() - fs3.statSync(file).mtimeMs > maxAgeMs) fs3.unlinkSync(file);
1133
+ const file = path4.join(dir, name);
1134
+ if (Date.now() - fs4.statSync(file).mtimeMs > maxAgeMs) fs4.unlinkSync(file);
1014
1135
  } catch {
1015
1136
  }
1016
1137
  }
@@ -1019,9 +1140,9 @@ var StreamBuffer = class {
1019
1140
 
1020
1141
  // src/internal/streaming/crash-recovery.ts
1021
1142
  var crypto2 = __toESM(require("crypto"));
1022
- var fs4 = __toESM(require("fs"));
1143
+ var fs5 = __toESM(require("fs"));
1023
1144
  var os4 = __toESM(require("os"));
1024
- var path4 = __toESM(require("path"));
1145
+ var path5 = __toESM(require("path"));
1025
1146
 
1026
1147
  // src/internal/support/errors.ts
1027
1148
  function errorMessage(error) {
@@ -1036,7 +1157,7 @@ function recoveryServer(serverUrl) {
1036
1157
  function recoveryFilePath(serverUrl, projectName) {
1037
1158
  const key = crypto2.createHash("sha256").update(`${recoveryServer(serverUrl)}
1038
1159
  ${projectName}`).digest("hex").slice(0, 16);
1039
- return path4.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
1160
+ return path5.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
1040
1161
  }
1041
1162
  function refusesRun(error) {
1042
1163
  return error instanceof HttpError && [400, 409, 422].includes(error.status);
@@ -1057,11 +1178,11 @@ var CrashRecovery = class {
1057
1178
  const file = { v: 1, serverUrl: this.serverUrl, payload };
1058
1179
  const temp = `${this.filePath}.${process.pid}.${crypto2.randomBytes(6).toString("hex")}.tmp`;
1059
1180
  try {
1060
- fs4.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
1061
- fs4.renameSync(temp, this.filePath);
1181
+ fs5.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
1182
+ fs5.renameSync(temp, this.filePath);
1062
1183
  this.logger.info("Saved recovery data for later upload");
1063
1184
  } catch (error) {
1064
- fs4.rmSync(temp, { force: true });
1185
+ fs5.rmSync(temp, { force: true });
1065
1186
  this.logger.error(`Failed to save recovery data: ${errorMessage(error)}`);
1066
1187
  }
1067
1188
  }
@@ -1072,26 +1193,26 @@ var CrashRecovery = class {
1072
1193
  load() {
1073
1194
  let fd;
1074
1195
  try {
1075
- fd = fs4.openSync(this.filePath, fs4.constants.O_RDONLY | (fs4.constants.O_NOFOLLOW ?? 0));
1196
+ fd = fs5.openSync(this.filePath, fs5.constants.O_RDONLY | (fs5.constants.O_NOFOLLOW ?? 0));
1076
1197
  } catch {
1077
1198
  return null;
1078
1199
  }
1079
1200
  try {
1080
- const stat = fs4.fstatSync(fd);
1201
+ const stat = fs5.fstatSync(fd);
1081
1202
  if (!stat.isFile() || typeof process.getuid === "function" && stat.uid !== process.getuid()) return null;
1082
- const file = JSON.parse(fs4.readFileSync(fd, "utf8"));
1203
+ const file = JSON.parse(fs5.readFileSync(fd, "utf8"));
1083
1204
  if (file?.v !== 1 || file.serverUrl !== this.serverUrl) return null;
1084
1205
  return file.payload && typeof file.payload === "object" ? file.payload : null;
1085
1206
  } catch {
1086
1207
  return null;
1087
1208
  } finally {
1088
- fs4.closeSync(fd);
1209
+ fs5.closeSync(fd);
1089
1210
  }
1090
1211
  }
1091
1212
  /** Delete the recovery file from disk */
1092
1213
  clear() {
1093
1214
  try {
1094
- fs4.rmSync(this.filePath, { force: true });
1215
+ fs5.rmSync(this.filePath, { force: true });
1095
1216
  } catch {
1096
1217
  }
1097
1218
  }
@@ -1123,25 +1244,25 @@ var CrashRecovery = class {
1123
1244
  };
1124
1245
 
1125
1246
  // src/internal/files/file-handler.ts
1126
- var fs6 = __toESM(require("fs"));
1247
+ var fs7 = __toESM(require("fs"));
1127
1248
  var os5 = __toESM(require("os"));
1128
- var path6 = __toESM(require("path"));
1249
+ var path7 = __toESM(require("path"));
1129
1250
  var crypto3 = __toESM(require("crypto"));
1130
1251
 
1131
1252
  // src/internal/files/compression.ts
1132
- var fs5 = __toESM(require("fs"));
1133
- var path5 = __toESM(require("path"));
1253
+ var fs6 = __toESM(require("fs"));
1254
+ var path6 = __toESM(require("path"));
1134
1255
  var zlib = __toESM(require("zlib"));
1135
1256
  var import_node_util = require("util");
1136
1257
  var gzipAsync = (0, import_node_util.promisify)(zlib.gzip);
1137
1258
  async function compressDirectory(sourceDir) {
1138
1259
  const files = [];
1139
1260
  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);
1261
+ for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
1262
+ const full = path6.join(dir, entry.name);
1263
+ const rel = path6.join(baseDir, entry.name);
1143
1264
  if (entry.isDirectory()) collect(full, rel);
1144
- else if (entry.isFile()) files.push({ path: rel, content: fs5.readFileSync(full) });
1265
+ else if (entry.isFile()) files.push({ path: rel, content: fs6.readFileSync(full) });
1145
1266
  }
1146
1267
  }
1147
1268
  collect(sourceDir);
@@ -1198,6 +1319,8 @@ var ATTACHMENT_NAMES = {
1198
1319
  codeReach: "piwi-code-reach",
1199
1320
  browserCoverage: "piwi-browser-coverage",
1200
1321
  browserFunctions: "piwi-browser-functions",
1322
+ cssCoverage: "piwi-css-coverage",
1323
+ cssRules: "piwi-css-rules",
1201
1324
  serverCoverageRequests: "piwi-server-coverage-requests",
1202
1325
  webVitals: "piwi-web-vitals",
1203
1326
  locatorSuggestion: "piwi-locator-suggestion",
@@ -1205,7 +1328,8 @@ var ATTACHMENT_NAMES = {
1205
1328
  userPick: "piwi-user-pick",
1206
1329
  aiUsage: "piwi-ai-usage",
1207
1330
  aiMeta: "piwi-ai-meta",
1208
- resources: "piwi-resources"
1331
+ resources: "piwi-resources",
1332
+ pauses: "piwi-pauses"
1209
1333
  };
1210
1334
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
1211
1335
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
@@ -1239,19 +1363,19 @@ var FileHandler = class {
1239
1363
  }
1240
1364
  /** Locate a Playwright HTML report directory containing `index.html`. Optionally override the search path. */
1241
1365
  findHTMLReportDirectory(customDir) {
1242
- const possibleDirs = customDir ? [customDir, path6.join(process.cwd(), customDir)] : ["playwright-report", "./playwright-report", path6.join(process.cwd(), "playwright-report")];
1366
+ const possibleDirs = customDir ? [customDir, path7.join(process.cwd(), customDir)] : ["playwright-report", "./playwright-report", path7.join(process.cwd(), "playwright-report")];
1243
1367
  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;
1368
+ if (fs7.existsSync(reportDir) && fs7.statSync(reportDir).isDirectory()) {
1369
+ if (fs7.existsSync(path7.join(reportDir, "index.html"))) return reportDir;
1246
1370
  }
1247
1371
  }
1248
1372
  return null;
1249
1373
  }
1250
1374
  /** Check if `dir` exists and is a directory; also checks `path.join(process.cwd(), dir)` */
1251
1375
  findReportDirectory(dir) {
1252
- const candidates = [dir, path6.join(process.cwd(), dir)];
1376
+ const candidates = [dir, path7.join(process.cwd(), dir)];
1253
1377
  for (const candidate of candidates) {
1254
- if (fs6.existsSync(candidate) && fs6.statSync(candidate).isDirectory()) return candidate;
1378
+ if (fs7.existsSync(candidate) && fs7.statSync(candidate).isDirectory()) return candidate;
1255
1379
  }
1256
1380
  return null;
1257
1381
  }
@@ -1269,7 +1393,7 @@ var FileHandler = class {
1269
1393
  const set = /* @__PURE__ */ new Set();
1270
1394
  if (testCase.attachments) {
1271
1395
  for (const a of testCase.attachments) {
1272
- if (a.name === "trace" && a.path) set.add(path6.resolve(a.path));
1396
+ if (a.name === "trace" && a.path) set.add(path7.resolve(a.path));
1273
1397
  }
1274
1398
  }
1275
1399
  return Array.from(set);
@@ -1297,24 +1421,24 @@ var FileHandler = class {
1297
1421
  cleanupBodyAttachments() {
1298
1422
  if (!this.bodyDir) return;
1299
1423
  try {
1300
- fs6.rmSync(this.bodyDir, { recursive: true, force: true });
1424
+ fs7.rmSync(this.bodyDir, { recursive: true, force: true });
1301
1425
  } catch (error) {
1302
1426
  this.logger.debug(`Could not remove temp attachments at ${this.bodyDir}: ${errorMessage(error)}`);
1303
1427
  }
1304
1428
  this.bodyDir = null;
1305
1429
  }
1306
1430
  resolvePathAttachment(a, testCase) {
1307
- if (!a.path || !fs6.existsSync(a.path)) return null;
1308
- const size = fs6.statSync(a.path).size;
1431
+ if (!a.path || !fs7.existsSync(a.path)) return null;
1432
+ const size = fs7.statSync(a.path).size;
1309
1433
  if (size > this.maxAttachmentBytes) {
1310
1434
  this.warnOversized(a, testCase, size);
1311
1435
  return null;
1312
1436
  }
1313
1437
  return {
1314
1438
  name: a.name || "attachment",
1315
- path: path6.resolve(a.path),
1439
+ path: path7.resolve(a.path),
1316
1440
  contentType: a.contentType || "application/octet-stream",
1317
- originalName: path6.basename(a.path)
1441
+ originalName: path7.basename(a.path)
1318
1442
  };
1319
1443
  }
1320
1444
  resolveBodyAttachment(a, testCase) {
@@ -1329,8 +1453,8 @@ var FileHandler = class {
1329
1453
  let filePath2 = this.bodyFiles.get(a);
1330
1454
  if (!filePath2) {
1331
1455
  try {
1332
- filePath2 = path6.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
1333
- fs6.writeFileSync(filePath2, body);
1456
+ filePath2 = path7.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
1457
+ fs7.writeFileSync(filePath2, body);
1334
1458
  this.bodyFiles.set(a, filePath2);
1335
1459
  } catch (error) {
1336
1460
  this.logger.warn(`Could not stage attachment "${a.name}" for upload: ${errorMessage(error)}`);
@@ -1340,7 +1464,7 @@ var FileHandler = class {
1340
1464
  return { name: a.name || "attachment", path: filePath2, contentType, originalName };
1341
1465
  }
1342
1466
  bodyAttachmentDir() {
1343
- if (!this.bodyDir) this.bodyDir = fs6.mkdtempSync(path6.join(os5.tmpdir(), "piwi-dashboard-attachments-"));
1467
+ if (!this.bodyDir) this.bodyDir = fs7.mkdtempSync(path7.join(os5.tmpdir(), "piwi-dashboard-attachments-"));
1344
1468
  return this.bodyDir;
1345
1469
  }
1346
1470
  warnOversized(a, testCase, size) {
@@ -1363,10 +1487,10 @@ var FileHandler = class {
1363
1487
  /** 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
1488
  parsePerformanceAttachments(testCase, attachments) {
1365
1489
  const find = (name) => attachments.find((a) => a.name === name);
1366
- const net = find(ATTACHMENT_NAMES.network);
1367
- if (net?.body) {
1490
+ const net3 = find(ATTACHMENT_NAMES.network);
1491
+ if (net3?.body) {
1368
1492
  try {
1369
- testCase.networkRequests = JSON.parse(net.body.toString());
1493
+ testCase.networkRequests = JSON.parse(net3.body.toString());
1370
1494
  } catch {
1371
1495
  }
1372
1496
  }
@@ -1422,17 +1546,17 @@ var FileHandler = class {
1422
1546
  const tracePaths = this.findTraceFiles(testCase);
1423
1547
  let lastPath = null;
1424
1548
  for (const tp of tracePaths) {
1425
- if (fs6.existsSync(tp)) lastPath = tp;
1549
+ if (fs7.existsSync(tp)) lastPath = tp;
1426
1550
  }
1427
1551
  if (!lastPath) return null;
1428
1552
  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);
1553
+ await new Promise((resolve11, reject) => {
1554
+ fs7.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve11).on("error", reject);
1431
1555
  });
1432
1556
  return {
1433
1557
  tracePath: lastPath,
1434
1558
  hash: hash.digest("hex"),
1435
- size: fs6.statSync(lastPath).size
1559
+ size: fs7.statSync(lastPath).size
1436
1560
  };
1437
1561
  }
1438
1562
  /** Ask the server which trace hashes it already has, returning the set of hashes that are missing */
@@ -1459,14 +1583,14 @@ function formatMiB(bytes) {
1459
1583
  var import_node_child_process = require("child_process");
1460
1584
 
1461
1585
  // src/internal/support/test-paths.ts
1462
- var path7 = __toESM(require("path"));
1586
+ var path8 = __toESM(require("path"));
1463
1587
  function configDirFromConfig(config) {
1464
- if (config?.configFile) return path7.dirname(config.configFile);
1588
+ if (config?.configFile) return path8.dirname(config.configFile);
1465
1589
  if (config?.rootDir) return config.rootDir;
1466
1590
  return process.cwd();
1467
1591
  }
1468
1592
  function relativeTestPath(file, base) {
1469
- return path7.relative(base, file).split(path7.sep).join("/");
1593
+ return path8.relative(base, file).split(path8.sep).join("/");
1470
1594
  }
1471
1595
 
1472
1596
  // src/internal/collect/metadata-collector.ts
@@ -1526,11 +1650,6 @@ function resolveScmBaseBranch(env) {
1526
1650
  const trimmed = target?.trim();
1527
1651
  return trimmed ? normalizeRef(trimmed) : void 0;
1528
1652
  }
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
1653
  function resolveRunOrigin(env, warn) {
1535
1654
  const raw = env[PIWI_ORIGIN_ENV.kind]?.trim();
1536
1655
  let kind = detectCi(env) ? "ci" : "local";
@@ -1769,7 +1888,7 @@ var MetadataCollector = class {
1769
1888
  };
1770
1889
 
1771
1890
  // src/internal/streaming/stream-manager.ts
1772
- var fs8 = __toESM(require("fs"));
1891
+ var fs9 = __toESM(require("fs"));
1773
1892
 
1774
1893
  // src/internal/streaming/bounded-queue.ts
1775
1894
  var DROP_TIER = {
@@ -1908,10 +2027,10 @@ function createLimiter(maxConcurrent) {
1908
2027
  if (run) run();
1909
2028
  };
1910
2029
  return function limit(fn) {
1911
- return new Promise((resolve9, reject) => {
2030
+ return new Promise((resolve11, reject) => {
1912
2031
  const run = () => {
1913
2032
  active++;
1914
- Promise.resolve().then(fn).then(resolve9, reject).finally(next);
2033
+ Promise.resolve().then(fn).then(resolve11, reject).finally(next);
1915
2034
  };
1916
2035
  if (active < limitValue) run();
1917
2036
  else queue.push(run);
@@ -1920,18 +2039,18 @@ function createLimiter(maxConcurrent) {
1920
2039
  }
1921
2040
 
1922
2041
  // src/internal/support/setup-file.ts
1923
- var path8 = __toESM(require("path"));
2042
+ var path9 = __toESM(require("path"));
1924
2043
  var os6 = __toESM(require("os"));
1925
- var fs7 = __toESM(require("fs"));
2044
+ var fs8 = __toESM(require("fs"));
1926
2045
  function getSetupFilePath(projectName) {
1927
- return path8.join(os6.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
2046
+ return path9.join(os6.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
1928
2047
  }
1929
2048
  function readSetupInfo(projectName) {
1930
2049
  const setupFile = getSetupFilePath(projectName);
1931
2050
  try {
1932
- if (fs7.existsSync(setupFile)) {
1933
- const info = JSON.parse(fs7.readFileSync(setupFile, "utf8"));
1934
- fs7.unlinkSync(setupFile);
2051
+ if (fs8.existsSync(setupFile)) {
2052
+ const info = JSON.parse(fs8.readFileSync(setupFile, "utf8"));
2053
+ fs8.unlinkSync(setupFile);
1935
2054
  if (info.projectName === projectName) return info;
1936
2055
  }
1937
2056
  } catch {
@@ -2009,6 +2128,8 @@ var StreamManager = class {
2009
2128
  this._token = null;
2010
2129
  this._auth = null;
2011
2130
  this._startPromise = null;
2131
+ /** Called when the dashboard says someone opened the run. */
2132
+ this.onWatched = null;
2012
2133
  const maxBytes = this.options.maxStreamBufferBytes ?? 0;
2013
2134
  this.pendingEvents = new BoundedEventQueue(maxBytes);
2014
2135
  this.pendingBeginEvents = new BoundedEventQueue(maxBytes);
@@ -2033,6 +2154,10 @@ var StreamManager = class {
2033
2154
  get startPromise() {
2034
2155
  return this._startPromise;
2035
2156
  }
2157
+ /** Whether anyone has the run open live, as the dashboard last said; `null` until it says. */
2158
+ get isWatched() {
2159
+ return this.watched;
2160
+ }
2036
2161
  /**
2037
2162
  * Whether a test-result (`complete`) event never reached the server through
2038
2163
  * the live stream: shed under buffer pressure, too large for any request the
@@ -2150,7 +2275,7 @@ var StreamManager = class {
2150
2275
  } catch (error) {
2151
2276
  if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
2152
2277
  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.`
2278
+ `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
2279
  );
2155
2280
  } else {
2156
2281
  this.logger.warn(
@@ -2283,8 +2408,11 @@ var StreamManager = class {
2283
2408
  if (this.pendingEvents.length > 0) this.flush();
2284
2409
  }, delay);
2285
2410
  }
2286
- // Take the dashboard's answer on whether anyone watches the run. An answer
2287
- // without the flag (an older dashboard) keeps step events flowing.
2411
+ /**
2412
+ * Take the dashboard's answer on whether anyone watches the run, from any
2413
+ * request of the stream. An answer without the flag (an older dashboard)
2414
+ * keeps step events flowing.
2415
+ */
2288
2416
  noteWatched(response) {
2289
2417
  const watched = response?.watched;
2290
2418
  const next = typeof watched === "boolean" ? watched : null;
@@ -2293,7 +2421,9 @@ var StreamManager = class {
2293
2421
  next ? "The run is open on the dashboard: streaming live steps." : "Nobody is watching the run: live steps are not sent."
2294
2422
  );
2295
2423
  }
2424
+ const opened = next === true && this.watched !== true;
2296
2425
  this.watched = next;
2426
+ if (opened) this.onWatched?.();
2297
2427
  }
2298
2428
  // Idle time after which a heartbeat goes out.
2299
2429
  heartbeatDelay() {
@@ -2387,7 +2517,7 @@ var StreamManager = class {
2387
2517
  this.logger.debugError(
2388
2518
  `${this.pendingEvents.length} events pending, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`
2389
2519
  );
2390
- await new Promise((resolve9) => setTimeout(resolve9, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
2520
+ await new Promise((resolve11) => setTimeout(resolve11, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
2391
2521
  }
2392
2522
  if (!this.pendingEvents.isEmpty) {
2393
2523
  const remaining = this.pendingEvents.takeAll();
@@ -2431,7 +2561,7 @@ var StreamManager = class {
2431
2561
  }
2432
2562
  /** 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
2563
  scheduleLiveUpload(tc) {
2434
- const hasTrace = !!this.options.uploadTraces && this.fileHandler.findTraceFiles(tc).some((p) => fs8.existsSync(p));
2564
+ const hasTrace = !!this.options.uploadTraces && this.fileHandler.findTraceFiles(tc).some((p) => fs9.existsSync(p));
2435
2565
  const hasAttachments = this.fileHandler.findAllAttachments(tc).length > 0;
2436
2566
  if (!hasTrace && !hasAttachments) return;
2437
2567
  const promise = (async () => {
@@ -2444,7 +2574,7 @@ var StreamManager = class {
2444
2574
  if (flush) await flush;
2445
2575
  const delays = [0, 1e3, 3e3];
2446
2576
  for (let attempt = 0; attempt < delays.length; attempt++) {
2447
- if (delays[attempt]) await new Promise((resolve9) => setTimeout(resolve9, delays[attempt]));
2577
+ if (delays[attempt]) await new Promise((resolve11) => setTimeout(resolve11, delays[attempt]));
2448
2578
  try {
2449
2579
  await this.limitLiveUpload(
2450
2580
  () => this.uploader.uploadCaseFiles(
@@ -3259,7 +3389,7 @@ function maskUrlFragment(hash) {
3259
3389
  }
3260
3390
 
3261
3391
  // ../core/src/locator-break.ts
3262
- var filePath = (path32) => path32.replace(/\\/g, "/").replace(/^\.\//, "");
3392
+ var filePath = (path34) => path34.replace(/\\/g, "/").replace(/^\.\//, "");
3263
3393
  function sameFilePath(a, b) {
3264
3394
  const x = filePath(a);
3265
3395
  const y = filePath(b);
@@ -3554,23 +3684,48 @@ function flattenSteps(steps, depth = 0) {
3554
3684
  }
3555
3685
  return result;
3556
3686
  }
3557
- function collectStepMetrics(steps, errors) {
3687
+ function epochMs(value) {
3688
+ if (value instanceof Date) return value.getTime();
3689
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
3690
+ }
3691
+ function pausedOverlap(start, duration, pauses) {
3692
+ const from = epochMs(start);
3693
+ const length = typeof duration === "number" && duration > 0 ? duration : 0;
3694
+ if (from === null || length === 0 || pauses.length === 0) return 0;
3695
+ const to = from + length;
3696
+ 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]);
3697
+ let ms = 0;
3698
+ let reached = from;
3699
+ for (const [a, b] of spans) {
3700
+ const begin = Math.max(a, reached);
3701
+ if (b > begin) ms += b - begin;
3702
+ reached = Math.max(reached, b);
3703
+ }
3704
+ return ms;
3705
+ }
3706
+ function unpausedDuration(step, pauses) {
3707
+ const duration = typeof step.duration === "number" && step.duration > 0 ? step.duration : 0;
3708
+ return duration - pausedOverlap(step.startTime, duration, pauses);
3709
+ }
3710
+ function collectStepMetrics(steps, errors, pauses = []) {
3558
3711
  const flatSteps = flattenSteps(steps);
3559
3712
  if (errors) markRecoveredSteps(flatSteps, errors);
3560
- const totalStepDuration = steps.reduce((sum, s) => sum + (s.duration || 0), 0);
3713
+ const own = (s) => pauses.length > 0 && s.duration > 0 ? unpausedDuration(s, pauses) : s.duration;
3714
+ const totalStepDuration = steps.reduce((sum, s) => sum + (own(s) || 0), 0);
3561
3715
  let slowestStep = null;
3562
3716
  for (const s of flatSteps) {
3563
- if (!slowestStep || s.duration > slowestStep.duration) slowestStep = { title: s.title, duration: s.duration };
3717
+ const duration = own(s);
3718
+ if (!slowestStep || duration > slowestStep.duration) slowestStep = { title: s.title, duration };
3564
3719
  }
3565
3720
  const navSteps = flatSteps.filter((s) => s.category === "navigation");
3566
3721
  const waitSteps = flatSteps.filter((s) => s.category === "wait");
3567
- const waitTotalDuration = waitSteps.reduce((sum, s) => sum + (s.duration || 0), 0);
3722
+ const waitTotalDuration = waitSteps.reduce((sum, s) => sum + (own(s) || 0), 0);
3568
3723
  return {
3569
3724
  steps: flatSteps,
3570
3725
  totalStepDuration,
3571
3726
  slowestStep,
3572
3727
  navigationCount: navSteps.length,
3573
- navigationTotalDuration: navSteps.reduce((sum, s) => sum + (s.duration || 0), 0),
3728
+ navigationTotalDuration: navSteps.reduce((sum, s) => sum + (own(s) || 0), 0),
3574
3729
  waitTotalDuration,
3575
3730
  waitCount: waitSteps.length
3576
3731
  };
@@ -3686,19 +3841,43 @@ function extractWaitEvents(steps, insideWait = false) {
3686
3841
  return events;
3687
3842
  }
3688
3843
 
3844
+ // src/internal/collect/pause-events.ts
3845
+ function readPauses(body) {
3846
+ if (!body) return [];
3847
+ try {
3848
+ const value = JSON.parse(body.toString("utf8"));
3849
+ if (!Array.isArray(value)) return [];
3850
+ return value.filter(
3851
+ (p) => !!p && typeof p === "object" && typeof p.location === "string" && typeof p.startedAt === "number" && typeof p.duration === "number"
3852
+ );
3853
+ } catch {
3854
+ return [];
3855
+ }
3856
+ }
3857
+ function pauseStepEvents(pauses) {
3858
+ return pauses.map((pause) => ({
3859
+ title: typeof pause.title === "string" ? pause.title : "",
3860
+ category: "pause",
3861
+ startedAt: pause.startedAt,
3862
+ duration: Math.max(0, pause.duration),
3863
+ status: typeof pause.reason === "string" ? pause.reason : "breakpoint",
3864
+ location: pause.location
3865
+ }));
3866
+ }
3867
+
3689
3868
  // src/internal/support/reporter-version.ts
3690
- var fs9 = __toESM(require("fs"));
3691
- var path9 = __toESM(require("path"));
3869
+ var fs10 = __toESM(require("fs"));
3870
+ var path10 = __toESM(require("path"));
3692
3871
  var cachedVersion = null;
3693
3872
  var CANDIDATE_OFFSETS = ["..", "../../.."];
3694
3873
  function findOwnPackageJson(fromDir) {
3695
3874
  for (const offset of CANDIDATE_OFFSETS) {
3696
3875
  try {
3697
- const pkgPath = path9.resolve(fromDir, offset, "package.json");
3698
- const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
3876
+ const pkgPath = path10.resolve(fromDir, offset, "package.json");
3877
+ const pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
3699
3878
  const { name, version } = pkg;
3700
3879
  if (name === "@piwitests/reporter" && typeof version === "string")
3701
- return { version, root: path9.dirname(pkgPath) };
3880
+ return { version, root: path10.dirname(pkgPath) };
3702
3881
  } catch {
3703
3882
  }
3704
3883
  }
@@ -3711,11 +3890,11 @@ function getReporterVersion() {
3711
3890
  }
3712
3891
 
3713
3892
  // src/internal/support/source-snippet.ts
3714
- var fs10 = __toESM(require("fs"));
3715
- var path10 = __toESM(require("path"));
3893
+ var fs11 = __toESM(require("fs"));
3894
+ var path11 = __toESM(require("path"));
3716
3895
  function readSourceSnippet(file, declLine, context, failingLine) {
3717
3896
  try {
3718
- const content = fs10.readFileSync(file, "utf-8");
3897
+ const content = fs11.readFileSync(file, "utf-8");
3719
3898
  const lines = content.split("\n");
3720
3899
  const anchor = failingLine ?? declLine;
3721
3900
  const start = Math.max(0, anchor - context - 1);
@@ -3744,7 +3923,7 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
3744
3923
  const maxFrames = opts.maxFrames ?? 4;
3745
3924
  const picked = [];
3746
3925
  const seen = /* @__PURE__ */ new Set();
3747
- const add = (absFile, line) => {
3926
+ const add2 = (absFile, line) => {
3748
3927
  const key = `${absFile}:${line}`;
3749
3928
  if (seen.has(key)) return;
3750
3929
  seen.add(key);
@@ -3755,66 +3934,40 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
3755
3934
  if (picked.length >= maxFrames) break;
3756
3935
  let abs;
3757
3936
  try {
3758
- abs = path10.resolve(frame.file);
3937
+ abs = path11.resolve(frame.file);
3759
3938
  } catch {
3760
3939
  continue;
3761
3940
  }
3762
- const rel = path10.relative(projectRoot, abs);
3763
- if (rel.startsWith("..") || path10.isAbsolute(rel)) continue;
3764
- if (!isNaN(frame.line)) add(abs, frame.line);
3941
+ const rel = path11.relative(projectRoot, abs);
3942
+ if (rel.startsWith("..") || path11.isAbsolute(rel)) continue;
3943
+ if (!isNaN(frame.line)) add2(abs, frame.line);
3765
3944
  }
3766
3945
  }
3767
- const absTest = path10.resolve(testFile2);
3946
+ const absTest = path11.resolve(testFile2);
3768
3947
  if (!picked.some((p) => p.absFile === absTest)) {
3769
- add(absTest, extractFailingLine(errorText, testFile2, declLine));
3948
+ add2(absTest, extractFailingLine(errorText, testFile2, declLine));
3770
3949
  }
3771
3950
  const frames = [];
3772
3951
  for (const p of picked.slice(0, maxFrames)) {
3773
3952
  const snippet = readSourceSnippet(p.absFile, p.line, context, p.line);
3774
3953
  if (snippet) {
3775
- frames.push({ file: path10.relative(projectRoot, p.absFile) || path10.basename(p.absFile), line: p.line, snippet });
3954
+ frames.push({ file: path11.relative(projectRoot, p.absFile) || path11.basename(p.absFile), line: p.line, snippet });
3776
3955
  }
3777
3956
  }
3778
3957
  return frames;
3779
3958
  }
3780
3959
  function extractFailingLine(errorText, testFile2, declarationLine) {
3781
3960
  if (!errorText) return declarationLine;
3782
- const expectedFile = path10.resolve(testFile2);
3961
+ const expectedFile = path11.resolve(testFile2);
3783
3962
  for (const frame of stackFrames(errorText)) {
3784
3963
  try {
3785
- if (path10.resolve(frame.file) === expectedFile && !isNaN(frame.line)) return frame.line;
3964
+ if (path11.resolve(frame.file) === expectedFile && !isNaN(frame.line)) return frame.line;
3786
3965
  } catch {
3787
3966
  }
3788
3967
  }
3789
3968
  return declarationLine;
3790
3969
  }
3791
3970
 
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
3971
  // src/internal/support/worker-index.ts
3819
3972
  function workerIndexOf(result) {
3820
3973
  if (!result) return null;
@@ -3900,28 +4053,28 @@ function isListMode(argv = process.argv) {
3900
4053
  }
3901
4054
 
3902
4055
  // src/public/global-setup.ts
3903
- var path14 = __toESM(require("path"));
3904
- var fs14 = __toESM(require("fs"));
4056
+ var path15 = __toESM(require("path"));
4057
+ var fs15 = __toESM(require("fs"));
3905
4058
 
3906
4059
  // src/internal/support/aria-sampling.ts
3907
- var path11 = __toESM(require("path"));
4060
+ var path12 = __toESM(require("path"));
3908
4061
  var os7 = __toESM(require("os"));
3909
- var fs11 = __toESM(require("fs"));
4062
+ var fs12 = __toESM(require("fs"));
3910
4063
  function ariaSampleIdentity(filePath2, title) {
3911
4064
  return `${filePath2}\0${title}`;
3912
4065
  }
3913
4066
  function getAriaSampleFilePath(projectName) {
3914
- return path11.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
4067
+ return path12.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
3915
4068
  }
3916
4069
  function writeAriaSampleFile(projectName, identities) {
3917
4070
  try {
3918
- fs11.writeFileSync(getAriaSampleFilePath(projectName), JSON.stringify({ projectName, identities }));
4071
+ fs12.writeFileSync(getAriaSampleFilePath(projectName), JSON.stringify({ projectName, identities }));
3919
4072
  } catch {
3920
4073
  }
3921
4074
  }
3922
4075
  function clearAriaSampleFile(projectName) {
3923
4076
  try {
3924
- fs11.rmSync(getAriaSampleFilePath(projectName), { force: true });
4077
+ fs12.rmSync(getAriaSampleFilePath(projectName), { force: true });
3925
4078
  } catch {
3926
4079
  }
3927
4080
  }
@@ -3930,7 +4083,7 @@ function loadAriaSampleSet(projectName) {
3930
4083
  if (cachedSets.has(projectName)) return cachedSets.get(projectName);
3931
4084
  let set = null;
3932
4085
  try {
3933
- const raw = fs11.readFileSync(getAriaSampleFilePath(projectName), "utf8");
4086
+ const raw = fs12.readFileSync(getAriaSampleFilePath(projectName), "utf8");
3934
4087
  const parsed2 = JSON.parse(raw);
3935
4088
  if (parsed2.projectName === projectName && Array.isArray(parsed2.identities)) {
3936
4089
  set = new Set(parsed2.identities.filter((x) => typeof x === "string"));
@@ -3952,6 +4105,7 @@ function isDueForAriaSample(testInfo) {
3952
4105
  }
3953
4106
 
3954
4107
  // src/internal/support/base-url-check.ts
4108
+ var net = __toESM(require("net"));
3955
4109
  var BASE_URL_CHECK_ATTEMPTS = 3;
3956
4110
  var BASE_URL_CHECK_TIMEOUT_MS = 1e4;
3957
4111
  var BASE_URL_CHECK_DELAY_MS = 2e3;
@@ -3973,9 +4127,9 @@ function cliProjectNames(argv = process.argv) {
3973
4127
  }
3974
4128
  return names;
3975
4129
  }
3976
- function wildcard(pattern) {
4130
+ function wildcard(pattern, flags = "i") {
3977
4131
  const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
3978
- return new RegExp(`^${escaped.join(".*")}$`, "i");
4132
+ return new RegExp(`^${escaped.join(".*")}$`, flags);
3979
4133
  }
3980
4134
  function selectedProjects(projects, argv = process.argv, filtered) {
3981
4135
  let chosen;
@@ -3985,7 +4139,7 @@ function selectedProjects(projects, argv = process.argv, filtered) {
3985
4139
  } else {
3986
4140
  const names = cliProjectNames(argv);
3987
4141
  if (!names || names.length === 0) return projects;
3988
- const patterns = names.map(wildcard);
4142
+ const patterns = names.map((name) => wildcard(name));
3989
4143
  chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
3990
4144
  }
3991
4145
  if (!argv.includes("--no-deps")) {
@@ -4007,23 +4161,58 @@ function asProxy(value) {
4007
4161
  const proxy = value;
4008
4162
  return proxy && typeof proxy === "object" && typeof proxy.server === "string" ? proxy : void 0;
4009
4163
  }
4010
- function baseUrlTargets(config, argv = process.argv) {
4164
+ function exposeNetworkCovers(rule, url) {
4165
+ if (!rule) return false;
4166
+ let parsed2;
4167
+ try {
4168
+ parsed2 = new URL(url);
4169
+ } catch {
4170
+ return false;
4171
+ }
4172
+ const host = parsed2.hostname;
4173
+ const port = parsed2.port ? Number(parsed2.port) : parsed2.protocol === "https:" ? 443 : 80;
4174
+ return rule.split(",").some((token) => {
4175
+ const match = token.match(/^(.*?)(?::(\d+))?$/);
4176
+ if (!match) return false;
4177
+ if (match[2] !== void 0 && Number(match[2]) !== port) return false;
4178
+ const tokenHost = match[1];
4179
+ if (tokenHost === "<loopback>") {
4180
+ return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "[::1]";
4181
+ }
4182
+ if (tokenHost === "*") return true;
4183
+ if (net.isIP(tokenHost)) return host === tokenHost;
4184
+ if (net.isIP(host)) return false;
4185
+ return wildcard(tokenHost.startsWith(".") ? `*${tokenHost}` : tokenHost, "").test(host);
4186
+ });
4187
+ }
4188
+ function reachedRemotely(use, url, env) {
4189
+ if (use?.connectOptions === void 0) {
4190
+ return !!env.PW_TEST_CONNECT_WS_ENDPOINT && !exposeNetworkCovers(env.PW_TEST_CONNECT_EXPOSE_NETWORK, url);
4191
+ }
4192
+ const connect2 = use.connectOptions;
4193
+ if (!connect2 || typeof connect2 !== "object" || typeof connect2.wsEndpoint !== "string") return false;
4194
+ return !exposeNetworkCovers(typeof connect2.exposeNetwork === "string" ? connect2.exposeNetwork : void 0, url);
4195
+ }
4196
+ function baseUrlTargets(config, argv = process.argv, env = process.env) {
4011
4197
  const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
4012
4198
  const filtered = config.projects?.length ? config.filteredProjects : void 0;
4013
4199
  const targets = /* @__PURE__ */ new Map();
4014
4200
  for (const project of selectedProjects(projects, argv, filtered)) {
4015
4201
  const url = project.use?.baseURL;
4016
4202
  if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
4203
+ const remoteBrowser = reachedRemotely(project.use, url, env);
4017
4204
  const target = targets.get(url);
4018
4205
  if (target) {
4019
4206
  if (project.name) target.projects.push(project.name);
4207
+ target.remoteBrowser ||= remoteBrowser;
4020
4208
  continue;
4021
4209
  }
4022
4210
  targets.set(url, {
4023
4211
  url,
4024
4212
  projects: project.name ? [project.name] : [],
4025
4213
  ignoreHTTPSErrors: project.use?.ignoreHTTPSErrors === true,
4026
- proxy: asProxy(project.use?.proxy)
4214
+ proxy: asProxy(project.use?.proxy),
4215
+ remoteBrowser
4027
4216
  });
4028
4217
  }
4029
4218
  return [...targets.values()];
@@ -4060,18 +4249,25 @@ async function checkBaseUrl(target, options = {}) {
4060
4249
  } else {
4061
4250
  reason = outcome.error;
4062
4251
  }
4063
- if (attempt < attempts) await new Promise((resolve9) => setTimeout(resolve9, delayMs));
4252
+ if (attempt < attempts) await new Promise((resolve11) => setTimeout(resolve11, delayMs));
4064
4253
  }
4065
4254
  return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
4066
4255
  }
4256
+ function remoteBrowserNote(target) {
4257
+ if (!target.remoteBrowser) return null;
4258
+ const { host } = new URL(target.url);
4259
+ return `The check ran from this machine's network, but a project's remote browser reaches ${host} from its own, since its exposeNetwork rule does not cover that address.`;
4260
+ }
4067
4261
  async function checkBaseUrls(config, options = {}) {
4068
- const targets = baseUrlTargets(config, options.argv);
4262
+ const targets = baseUrlTargets(config, options.argv, options.env);
4069
4263
  const reasons = await Promise.all(targets.map((target) => checkBaseUrl(target, options)));
4070
4264
  const down = targets.flatMap((target, i) => reasons[i] ? [{ target, reason: reasons[i] }] : []);
4071
4265
  if (down.length === 0) return targets;
4072
4266
  const lines = down.map(({ target, reason }) => {
4073
4267
  const used = target.projects.length > 0 ? ` (${target.projects.join(", ")})` : "";
4074
- return ` ${target.url}${used}: ${reason}`;
4268
+ const note = remoteBrowserNote(target);
4269
+ return ` ${target.url}${used}: ${reason}${note ? `
4270
+ ${note}` : ""}`;
4075
4271
  });
4076
4272
  const message = [
4077
4273
  `[Piwi Dashboard] The app under test did not answer, so no test ran:`,
@@ -4084,14 +4280,14 @@ async function checkBaseUrls(config, options = {}) {
4084
4280
  }
4085
4281
 
4086
4282
  // src/internal/manifest/collect.ts
4087
- var fs12 = __toESM(require("fs"));
4088
- var path12 = __toESM(require("path"));
4283
+ var fs13 = __toESM(require("fs"));
4284
+ var path13 = __toESM(require("path"));
4089
4285
 
4090
4286
  // src/internal/transport/backend-request.ts
4091
4287
  var http2 = __toESM(require("http"));
4092
4288
  var https2 = __toESM(require("https"));
4093
4289
  function backendRequest(url, init) {
4094
- return new Promise((resolve9, reject) => {
4290
+ return new Promise((resolve11, reject) => {
4095
4291
  const target = new URL(url);
4096
4292
  const headers = { ...init.headers };
4097
4293
  if (init.body !== void 0) headers["Content-Length"] = Buffer.byteLength(init.body);
@@ -4106,7 +4302,7 @@ function backendRequest(url, init) {
4106
4302
  });
4107
4303
  res.on(
4108
4304
  "end",
4109
- () => resolve9({ status: res.statusCode ?? 0, headers: res.headers, text: Buffer.concat(chunks).toString("utf8") })
4305
+ () => resolve11({ status: res.statusCode ?? 0, headers: res.headers, text: Buffer.concat(chunks).toString("utf8") })
4110
4306
  );
4111
4307
  res.on("error", reject);
4112
4308
  res.on("close", () => {
@@ -4151,10 +4347,10 @@ function hasInstrumentationHeader(headers) {
4151
4347
  return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
4152
4348
  }
4153
4349
  function readCommittedManifest(configDir) {
4154
- const file = path12.join(configDir, COMMITTED_MANIFEST_FILE);
4350
+ const file = path13.join(configDir, COMMITTED_MANIFEST_FILE);
4155
4351
  try {
4156
- if (!fs12.existsSync(file)) return null;
4157
- return parseManifestJson(fs12.readFileSync(file, "utf8"));
4352
+ if (!fs13.existsSync(file)) return null;
4353
+ return parseManifestJson(fs13.readFileSync(file, "utf8"));
4158
4354
  } catch {
4159
4355
  return null;
4160
4356
  }
@@ -4180,8 +4376,8 @@ function backendManifestFetch(insecure) {
4180
4376
  function instrumentationManifestUrls(baseUrl) {
4181
4377
  const base = new URL(baseUrl);
4182
4378
  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];
4379
+ const path34 = base.pathname.replace(/\/+$/, "");
4380
+ return path34 ? [new URL(`${path34}${INSTRUMENTATION_MANIFEST_PATH}`, base).toString(), root] : [root];
4185
4381
  }
4186
4382
  async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
4187
4383
  try {
@@ -4319,13 +4515,13 @@ function normalizeFunctionRanges(ranges) {
4319
4515
  }
4320
4516
  function normalizeCoveragePath(raw) {
4321
4517
  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;
4518
+ const path34 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4519
+ if (!path34 || path34.startsWith("../") || path34.startsWith("/") || /^[A-Za-z]:\//.test(path34)) return null;
4520
+ if (/(^|\/)node_modules\//.test(path34)) return null;
4521
+ return path34;
4326
4522
  }
4327
- function fileEntry(path32, runtime, blob, functions = [], lines = []) {
4328
- const file = { path: path32, runtime };
4523
+ function fileEntry(path34, runtime, blob, functions = [], lines = []) {
4524
+ const file = { path: path34, runtime };
4329
4525
  if (blob && (functions.length > 0 || lines.length > 0)) file.blob = blob;
4330
4526
  if (functions.length > 0) file.functions = functions;
4331
4527
  if (lines.length > 0) file.lines = lines;
@@ -4344,11 +4540,11 @@ function sanitizeCoverage(raw) {
4344
4540
  const files = [];
4345
4541
  for (const item of raw.files) {
4346
4542
  if (!isRecord(item) || !isRuntime(item.runtime)) continue;
4347
- const path32 = normalizeCoveragePath(item.path);
4348
- if (path32 === null) continue;
4543
+ const path34 = normalizeCoveragePath(item.path);
4544
+ if (path34 === null) continue;
4349
4545
  const blob = typeof item.blob === "string" && BLOB_ID.test(item.blob) ? item.blob : void 0;
4350
4546
  files.push(
4351
- fileEntry(path32, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
4547
+ fileEntry(path34, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
4352
4548
  );
4353
4549
  }
4354
4550
  const merged = mergeCoverage(captureOf(files, raw.incomplete === true));
@@ -4413,8 +4609,8 @@ function reduceCoverage(capture, granularity) {
4413
4609
 
4414
4610
  // src/internal/collect/source-checksums.ts
4415
4611
  var crypto4 = __toESM(require("crypto"));
4416
- var fs13 = __toESM(require("fs"));
4417
- var path13 = __toESM(require("path"));
4612
+ var fs14 = __toESM(require("fs"));
4613
+ var path14 = __toESM(require("path"));
4418
4614
  var CHECKSUM = /^(sha1|sha256):[0-9a-f]{40,64}$/;
4419
4615
  function checksumsOf(raw) {
4420
4616
  if (!Array.isArray(raw)) return [];
@@ -4479,10 +4675,10 @@ var SourceChecksums = class {
4479
4675
  }
4480
4676
  async read(relativePath2) {
4481
4677
  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));
4678
+ const root = path14.resolve(this.repoRoot());
4679
+ const file = path14.resolve(root, relativePath2);
4680
+ if (!file.startsWith(root + path14.sep)) return null;
4681
+ return contentChecksums(await fs14.promises.readFile(file));
4486
4682
  } catch {
4487
4683
  return null;
4488
4684
  }
@@ -4576,7 +4772,7 @@ function instanceOf(answer) {
4576
4772
  return { id: instance.id, startedAt };
4577
4773
  }
4578
4774
  function processKeyOf(id) {
4579
- const dot = id.lastIndexOf(".");
4775
+ const dot = id.indexOf(".");
4580
4776
  return dot > 0 ? id.slice(0, dot) : null;
4581
4777
  }
4582
4778
  function withTestCoverage(files, tests) {
@@ -4627,13 +4823,13 @@ var ServerCoverageCollector = class {
4627
4823
  if (parts.length === 0 || this.closer.signal.aborted) {
4628
4824
  return Promise.resolve({ files: [], incomplete: truncated || parts.length > 0 });
4629
4825
  }
4630
- return new Promise((resolve9) => {
4826
+ return new Promise((resolve11) => {
4631
4827
  const entry = {
4632
4828
  pending: parts.length,
4633
4829
  captures: [],
4634
4830
  checksums: /* @__PURE__ */ new Map(),
4635
4831
  incomplete: truncated,
4636
- resolve: resolve9
4832
+ resolve: resolve11
4637
4833
  };
4638
4834
  for (const [origin, ids] of parts) this.enqueue(origin, { entry, ids });
4639
4835
  });
@@ -4959,14 +5155,14 @@ var ServerCoverageCollector = class {
4959
5155
  // src/public/global-setup.ts
4960
5156
  function createGlobalSetup(options, userSetup) {
4961
5157
  return async function globalSetupFn(config) {
4962
- const piwiReporterPath = path14.resolve(__dirname, "./index.js");
5158
+ const piwiReporterPath = path15.resolve(__dirname, "./index.js");
4963
5159
  let inlineReporterOptions = {};
4964
5160
  if (Array.isArray(config?.reporter)) {
4965
5161
  for (const r of config.reporter) {
4966
5162
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
4967
5163
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
4968
5164
  try {
4969
- return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
5165
+ return path15.resolve(require.resolve(r[0])) === piwiReporterPath;
4970
5166
  } catch {
4971
5167
  return false;
4972
5168
  }
@@ -4977,7 +5173,10 @@ function createGlobalSetup(options, userSetup) {
4977
5173
  }
4978
5174
  }
4979
5175
  }
4980
- const opts = resolveOptions({ ...inlineReporterOptions, ...options });
5176
+ const opts = resolveOptions(
5177
+ { ...inlineReporterOptions, ...options },
5178
+ configDirFromConfig(config)
5179
+ );
4981
5180
  const logger = new Logger(opts.verbose ?? false);
4982
5181
  if (isUiMode()) {
4983
5182
  logger.debug("UI mode detected \u2014 skipping run registration.");
@@ -4991,7 +5190,16 @@ function createGlobalSetup(options, userSetup) {
4991
5190
  }
4992
5191
  if (opts.checkBaseUrl === true) {
4993
5192
  const checked = await checkBaseUrls(config ?? {});
4994
- for (const target of checked) logger.debug(`Base URL answered: ${target.url}`);
5193
+ for (const target of checked) {
5194
+ const note = remoteBrowserNote(target);
5195
+ if (note) {
5196
+ logger.info(
5197
+ `Base URL answered: ${target.url}. ${note} A navigation failure from that browser can mean its machine cannot reach the app, not that the app is down.`
5198
+ );
5199
+ } else {
5200
+ logger.debug(`Base URL answered: ${target.url}`);
5201
+ }
5202
+ }
4995
5203
  }
4996
5204
  if (opts.enabled === false || !opts.serverUrl) {
4997
5205
  logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
@@ -5002,7 +5210,7 @@ function createGlobalSetup(options, userSetup) {
5002
5210
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
5003
5211
  if (r[0].toLowerCase().includes("piwi")) return true;
5004
5212
  try {
5005
- return path14.resolve(require.resolve(r[0])) === piwiReporterPath;
5213
+ return path15.resolve(require.resolve(r[0])) === piwiReporterPath;
5006
5214
  } catch {
5007
5215
  return false;
5008
5216
  }
@@ -5032,7 +5240,7 @@ function createGlobalSetup(options, userSetup) {
5032
5240
  auth
5033
5241
  );
5034
5242
  if (response?.runId && response?.setupToken) {
5035
- fs14.writeFileSync(
5243
+ fs15.writeFileSync(
5036
5244
  getSetupFilePath(opts.projectName),
5037
5245
  JSON.stringify({
5038
5246
  runId: response.runId,
@@ -5086,11 +5294,11 @@ function createGlobalSetup(options, userSetup) {
5086
5294
  }
5087
5295
 
5088
5296
  // src/public/config-wrapper.ts
5089
- var path15 = __toESM(require("path"));
5090
- var fs17 = __toESM(require("fs"));
5297
+ var path16 = __toESM(require("path"));
5298
+ var fs18 = __toESM(require("fs"));
5091
5299
 
5092
5300
  // src/internal/probe/mode.ts
5093
- var fs15 = __toESM(require("fs"));
5301
+ var fs16 = __toESM(require("fs"));
5094
5302
 
5095
5303
  // src/internal/probe/plan.ts
5096
5304
  function probeRunMetadata(base = {}) {
@@ -5102,8 +5310,8 @@ function parseRouteKey(routeKey2) {
5102
5310
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
5103
5311
  }
5104
5312
  function patternToRegex(pattern) {
5105
- const path32 = pattern.split("?")[0] ?? pattern;
5106
- const escaped = path32.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
5313
+ const path34 = pattern.split("?")[0] ?? pattern;
5314
+ const escaped = path34.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
5107
5315
  return new RegExp(`^${escaped}$`);
5108
5316
  }
5109
5317
  function suitePathEqual(a, b) {
@@ -5126,13 +5334,13 @@ function matchProbeItem(plan, test) {
5126
5334
  function requestMatchesRoute(method, url, routeKey2) {
5127
5335
  const { method: rm, pattern } = parseRouteKey(routeKey2);
5128
5336
  if (rm && rm !== method.toUpperCase()) return false;
5129
- let path32;
5337
+ let path34;
5130
5338
  try {
5131
- path32 = new URL(url).pathname;
5339
+ path34 = new URL(url).pathname;
5132
5340
  } catch {
5133
- path32 = url.split("?")[0] ?? url;
5341
+ path34 = url.split("?")[0] ?? url;
5134
5342
  }
5135
- return patternToRegex(pattern).test(path32);
5343
+ return patternToRegex(pattern).test(path34);
5136
5344
  }
5137
5345
  function createProbeState() {
5138
5346
  return { navigated: false, matches: 0 };
@@ -5154,10 +5362,10 @@ function isProbeMode() {
5154
5362
  function loadPlan() {
5155
5363
  if (planCache !== void 0) return planCache;
5156
5364
  planCache = null;
5157
- const path32 = process.env[PIWI_PROBE_ENV.plan];
5158
- if (path32) {
5365
+ const path34 = process.env[PIWI_PROBE_ENV.plan];
5366
+ if (path34) {
5159
5367
  try {
5160
- planCache = JSON.parse(fs15.readFileSync(path32, "utf8"));
5368
+ planCache = JSON.parse(fs16.readFileSync(path34, "utf8"));
5161
5369
  } catch {
5162
5370
  planCache = null;
5163
5371
  }
@@ -5181,17 +5389,17 @@ function outcomeFromStatus(status, applied) {
5181
5389
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
5182
5390
  }
5183
5391
  function recordProbeOutcome(line) {
5184
- const path32 = process.env[PIWI_PROBE_ENV.results];
5185
- if (!path32) return;
5392
+ const path34 = process.env[PIWI_PROBE_ENV.results];
5393
+ if (!path34) return;
5186
5394
  try {
5187
- fs15.appendFileSync(path32, `${JSON.stringify(line)}
5395
+ fs16.appendFileSync(path34, `${JSON.stringify(line)}
5188
5396
  `);
5189
5397
  } catch {
5190
5398
  }
5191
5399
  }
5192
5400
 
5193
5401
  // src/internal/flake/mode.ts
5194
- var fs16 = __toESM(require("fs"));
5402
+ var fs17 = __toESM(require("fs"));
5195
5403
 
5196
5404
  // ../core/src/error-signature.ts
5197
5405
  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 +5775,7 @@ function originOf2(url) {
5567
5775
  return null;
5568
5776
  }
5569
5777
  }
5570
- var sleep = (ms) => new Promise((resolve9) => setTimeout(resolve9, Math.max(0, ms)));
5778
+ var sleep = (ms) => new Promise((resolve11) => setTimeout(resolve11, Math.max(0, ms)));
5571
5779
  async function performRouteAction(route, action, startedAt) {
5572
5780
  switch (action.kind) {
5573
5781
  case "abort":
@@ -5691,7 +5899,7 @@ function loadFlakePlan() {
5691
5899
  if (planCache2 && planCache2.file === file) return planCache2.plan;
5692
5900
  let text;
5693
5901
  try {
5694
- text = fs16.readFileSync(file, "utf8");
5902
+ text = fs17.readFileSync(file, "utf8");
5695
5903
  } catch (error) {
5696
5904
  throw new Error(`[Piwi Dashboard] Cannot read the flake plan ${file}: ${errorMessage(error)}`);
5697
5905
  }
@@ -5852,7 +6060,7 @@ function recordFlakeResult(line) {
5852
6060
  const file = process.env[PIWI_FLAKE_ENV.results]?.trim();
5853
6061
  if (!file) return;
5854
6062
  try {
5855
- fs16.appendFileSync(file, `${JSON.stringify(line)}
6063
+ fs17.appendFileSync(file, `${JSON.stringify(line)}
5856
6064
  `);
5857
6065
  } catch {
5858
6066
  }
@@ -5923,11 +6131,11 @@ function injectReporter(reporter, piwiOptions) {
5923
6131
  }
5924
6132
  function resolveSetupModule() {
5925
6133
  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")
6134
+ path16.join(__dirname, "global-setup-module.js"),
6135
+ path16.join(__dirname, "..", "global-setup-module.js"),
6136
+ path16.join(__dirname, "..", "global-setup-module.ts")
5929
6137
  ];
5930
- return candidates.find((candidate) => fs17.existsSync(candidate)) ?? candidates[0];
6138
+ return candidates.find((candidate) => fs18.existsSync(candidate)) ?? candidates[0];
5931
6139
  }
5932
6140
  function wrapConfig(config, piwiOptions) {
5933
6141
  if (piwiOptions) applyOptionsToEnv(piwiOptions);
@@ -6323,8 +6531,8 @@ function buildErrorText(result, base = process.cwd()) {
6323
6531
  var import_node_crypto2 = require("crypto");
6324
6532
 
6325
6533
  // src/internal/support/ci-output.ts
6326
- var fs18 = __toESM(require("fs"));
6327
- var path16 = __toESM(require("path"));
6534
+ var fs19 = __toESM(require("fs"));
6535
+ var path17 = __toESM(require("path"));
6328
6536
  var SUMMARY_MAX_FAILURES = 20;
6329
6537
  function emitRunOutputs(output, logger, outputFile, env = process.env) {
6330
6538
  logger.info(`View run: ${output.runUrl}`);
@@ -6334,9 +6542,9 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
6334
6542
  }
6335
6543
  function writeOutputFile(file, output, logger) {
6336
6544
  try {
6337
- const dir = path16.dirname(file);
6338
- if (dir && dir !== ".") fs18.mkdirSync(dir, { recursive: true });
6339
- fs18.writeFileSync(
6545
+ const dir = path17.dirname(file);
6546
+ if (dir && dir !== ".") fs19.mkdirSync(dir, { recursive: true });
6547
+ fs19.writeFileSync(
6340
6548
  file,
6341
6549
  JSON.stringify(
6342
6550
  {
@@ -6416,7 +6624,7 @@ function emitGitLabDotenv(output, env, logger) {
6416
6624
  if (output.projectId != null) lines.push(`PIWI_PROJECT_ID=${output.projectId}`);
6417
6625
  if (output.ciBuildUrl) lines.push(`PIWI_CI_BUILD_URL=${output.ciBuildUrl}`);
6418
6626
  try {
6419
- fs18.writeFileSync(file, lines.join("\n") + "\n");
6627
+ fs19.writeFileSync(file, lines.join("\n") + "\n");
6420
6628
  logger.info(`Wrote GitLab dotenv report to ${file} (declare it as artifacts:reports:dotenv)`);
6421
6629
  } catch (error) {
6422
6630
  logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
@@ -6433,7 +6641,7 @@ function emitResourceSummary(lines, logger, env = process.env) {
6433
6641
  }
6434
6642
  function appendFileLines(file, lines, logger, label) {
6435
6643
  try {
6436
- fs18.appendFileSync(file, lines.join("\n") + "\n");
6644
+ fs19.appendFileSync(file, lines.join("\n") + "\n");
6437
6645
  } catch (error) {
6438
6646
  logger.warn(`Failed to write GitHub Actions ${label}: ${errorMessage(error)}`);
6439
6647
  }
@@ -6446,8 +6654,8 @@ function ciBuildUrlFromMetadata(metadata) {
6446
6654
 
6447
6655
  // src/internal/submit/run-submitter.ts
6448
6656
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
6449
- function markSubmitFallback(run, path32, reason) {
6450
- run.metadata.ingestHealth = { submitFallback: { path: path32, reason } };
6657
+ function markSubmitFallback(run, path34, reason) {
6658
+ run.metadata.ingestHealth = { submitFallback: { path: path34, reason } };
6451
6659
  }
6452
6660
  var RunSubmitter = class {
6453
6661
  /**
@@ -6708,7 +6916,7 @@ var RunSubmitter = class {
6708
6916
  */
6709
6917
  stopForMissingAuth(run, payload) {
6710
6918
  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.`
6919
+ `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
6920
  );
6713
6921
  this.logRecoverySaved();
6714
6922
  this.saveRecovery(payload);
@@ -6723,13 +6931,13 @@ var RunSubmitter = class {
6723
6931
  };
6724
6932
 
6725
6933
  // src/internal/capture/resource-ledger.ts
6726
- var fs20 = __toESM(require("fs"));
6727
- var path19 = __toESM(require("path"));
6934
+ var fs21 = __toESM(require("fs"));
6935
+ var path20 = __toESM(require("path"));
6728
6936
  var import_node_module2 = require("module");
6729
6937
 
6730
6938
  // src/internal/capture/quiet-capture.ts
6731
6939
  var import_node_module = require("module");
6732
- var path17 = __toESM(require("path"));
6940
+ var path18 = __toESM(require("path"));
6733
6941
  var BOXED_PREFIX_EXPORTS = [
6734
6942
  [
6735
6943
  "playwright-core/lib/coreBundle",
@@ -6766,7 +6974,7 @@ function resolveBoxing() {
6766
6974
  if (typeof set !== "function") continue;
6767
6975
  return {
6768
6976
  set,
6769
- prefixes: [path17.dirname(playwrightPackageJson), path17.join(root, "dist") + path17.sep]
6977
+ prefixes: [path18.dirname(playwrightPackageJson), path18.join(root, "dist") + path18.sep]
6770
6978
  };
6771
6979
  }
6772
6980
  } catch {
@@ -6798,12 +7006,12 @@ function internalCall(owner, run) {
6798
7006
  var import_node_perf_hooks = require("perf_hooks");
6799
7007
 
6800
7008
  // src/internal/support/system-readers.ts
6801
- var fs19 = __toESM(require("fs"));
6802
- var path18 = __toESM(require("path"));
7009
+ var fs20 = __toESM(require("fs"));
7010
+ var path19 = __toESM(require("path"));
6803
7011
  var CLK_TCK = 100;
6804
7012
  function readText(file) {
6805
7013
  try {
6806
- return fs19.readFileSync(file, "utf8");
7014
+ return fs20.readFileSync(file, "utf8");
6807
7015
  } catch {
6808
7016
  return null;
6809
7017
  }
@@ -6937,45 +7145,45 @@ var ProcFs = class {
6937
7145
  }
6938
7146
  /** Whether this looks like a Linux `/proc` at all. */
6939
7147
  available() {
6940
- return fs19.existsSync(path18.join(this.root, "stat"));
7148
+ return fs20.existsSync(path19.join(this.root, "stat"));
6941
7149
  }
6942
7150
  /** Every process, keyed by pid. */
6943
7151
  table() {
6944
7152
  const out = /* @__PURE__ */ new Map();
6945
7153
  let names;
6946
7154
  try {
6947
- names = fs19.readdirSync(this.root);
7155
+ names = fs20.readdirSync(this.root);
6948
7156
  } catch {
6949
7157
  return out;
6950
7158
  }
6951
7159
  for (const name of names) {
6952
7160
  if (!/^\d+$/.test(name)) continue;
6953
- const text = readText(path18.join(this.root, name, "stat"));
7161
+ const text = readText(path19.join(this.root, name, "stat"));
6954
7162
  const stat = text ? parseProcStat(Number(name), text) : null;
6955
7163
  if (stat) out.set(stat.pid, stat);
6956
7164
  }
6957
7165
  return out;
6958
7166
  }
6959
7167
  stat(pid) {
6960
- const text = readText(path18.join(this.root, String(pid), "stat"));
7168
+ const text = readText(path19.join(this.root, String(pid), "stat"));
6961
7169
  return text ? parseProcStat(pid, text) : null;
6962
7170
  }
6963
7171
  runWaitMs(pid) {
6964
- const text = readText(path18.join(this.root, String(pid), "schedstat"));
7172
+ const text = readText(path19.join(this.root, String(pid), "schedstat"));
6965
7173
  return text ? parseSchedstat(text) : null;
6966
7174
  }
6967
7175
  cmdline(pid) {
6968
- return (readText(path18.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
7176
+ return (readText(path19.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
6969
7177
  }
6970
7178
  /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
6971
7179
  statusKb(pid, key) {
6972
- const text = readText(path18.join(this.root, String(pid), "status"));
7180
+ const text = readText(path19.join(this.root, String(pid), "status"));
6973
7181
  return text ? parseKbField(text, key) : null;
6974
7182
  }
6975
7183
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
6976
7184
  async pssKb(pid) {
6977
7185
  try {
6978
- return parseKbField(await fs19.promises.readFile(path18.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
7186
+ return parseKbField(await fs20.promises.readFile(path19.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
6979
7187
  } catch {
6980
7188
  return null;
6981
7189
  }
@@ -6983,29 +7191,29 @@ var ProcFs = class {
6983
7191
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
6984
7192
  resetPeakRss(pid) {
6985
7193
  try {
6986
- fs19.writeFileSync(path18.join(this.root, String(pid), "clear_refs"), "5");
7194
+ fs20.writeFileSync(path19.join(this.root, String(pid), "clear_refs"), "5");
6987
7195
  return true;
6988
7196
  } catch {
6989
7197
  return false;
6990
7198
  }
6991
7199
  }
6992
7200
  cpuTicks() {
6993
- const text = readText(path18.join(this.root, "stat"));
7201
+ const text = readText(path19.join(this.root, "stat"));
6994
7202
  return text ? parseCpuTicks(text) : null;
6995
7203
  }
6996
7204
  /** Total stall time of a resource's pressure file, in µs. */
6997
7205
  pressureUs(resource) {
6998
- const text = readText(path18.join(this.root, "pressure", resource));
7206
+ const text = readText(path19.join(this.root, "pressure", resource));
6999
7207
  return text ? parsePsiTotal(text) : null;
7000
7208
  }
7001
7209
  memAvailableKb() {
7002
- const text = readText(path18.join(this.root, "meminfo"));
7210
+ const text = readText(path19.join(this.root, "meminfo"));
7003
7211
  return text ? parseKbField(text, "MemAvailable") : null;
7004
7212
  }
7005
7213
  /** Open file descriptors of a process. */
7006
7214
  fdCount(pid) {
7007
7215
  try {
7008
- return fs19.readdirSync(path18.join(this.root, String(pid), "fd")).length;
7216
+ return fs20.readdirSync(path19.join(this.root, String(pid), "fd")).length;
7009
7217
  } catch {
7010
7218
  return null;
7011
7219
  }
@@ -7026,7 +7234,7 @@ var CgroupFs = class {
7026
7234
  }
7027
7235
  /** The directory of a controller (v1) or of the unified hierarchy (v2), when it exists. */
7028
7236
  dirs() {
7029
- const text = readText(path18.join(this.procRoot, String(this.pid), "cgroup"));
7237
+ const text = readText(path19.join(this.procRoot, String(this.pid), "cgroup"));
7030
7238
  if (!text) return null;
7031
7239
  const v1 = /* @__PURE__ */ new Map();
7032
7240
  let v2 = null;
@@ -7037,21 +7245,21 @@ var CgroupFs = class {
7037
7245
  else for (const controller of match[1].split(",")) v1.set(controller, match[2]);
7038
7246
  }
7039
7247
  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;
7248
+ const nested = path19.join(mount, sub);
7249
+ if (fs20.existsSync(nested)) return nested;
7250
+ return fs20.existsSync(mount) ? mount : null;
7043
7251
  };
7044
7252
  if (v1.has("memory") || v1.has("cpu")) {
7045
7253
  const memoryPath = v1.get("memory");
7046
7254
  const cpuPath = v1.get("cpu");
7047
- const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path18.join(this.cgroupRoot, name)).find((dir) => fs19.existsSync(dir));
7255
+ const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path19.join(this.cgroupRoot, name)).find((dir) => fs20.existsSync(dir));
7048
7256
  return {
7049
7257
  version: 1,
7050
- memory: memoryPath !== void 0 ? pick(path18.join(this.cgroupRoot, "memory"), memoryPath) : null,
7258
+ memory: memoryPath !== void 0 ? pick(path19.join(this.cgroupRoot, "memory"), memoryPath) : null,
7051
7259
  cpu: cpuPath !== void 0 && cpuMount ? pick(cpuMount, cpuPath) : null
7052
7260
  };
7053
7261
  }
7054
- if (v2 !== null && fs19.existsSync(path18.join(this.cgroupRoot, "cgroup.controllers"))) {
7262
+ if (v2 !== null && fs20.existsSync(path19.join(this.cgroupRoot, "cgroup.controllers"))) {
7055
7263
  const dir = pick(this.cgroupRoot, v2);
7056
7264
  return { version: 2, memory: dir, cpu: dir };
7057
7265
  }
@@ -7060,7 +7268,7 @@ var CgroupFs = class {
7060
7268
  read() {
7061
7269
  const dirs = this.dirs();
7062
7270
  if (!dirs) return null;
7063
- const file = (dir, name) => dir ? readText(path18.join(dir, name)) : null;
7271
+ const file = (dir, name) => dir ? readText(path19.join(dir, name)) : null;
7064
7272
  if (dirs.version === 2) {
7065
7273
  const max = file(dirs.memory, "memory.max");
7066
7274
  const cpuMax = file(dirs.cpu, "cpu.max")?.trim().split(/\s+/);
@@ -7246,10 +7454,10 @@ var PAGE_READ_TIMEOUT_MS = 1e3;
7246
7454
  var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
7247
7455
  var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
7248
7456
  function relativePath(file) {
7249
- return path19.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path19.sep).join("/");
7457
+ return path20.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path20.sep).join("/");
7250
7458
  }
7251
7459
  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;
7460
+ return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path20.sep}node_modules${path20.sep}`) || file.includes("/node_modules/") || file === __filename;
7253
7461
  }
7254
7462
  function userSite(stack) {
7255
7463
  for (const line of (stack ?? "").split("\n").slice(1)) {
@@ -7297,7 +7505,7 @@ function playwrightTestInfoSource() {
7297
7505
  try {
7298
7506
  const packageJson = resolvePlaywrightPackageJson();
7299
7507
  if (!packageJson) return () => null;
7300
- const globals = (0, import_node_module2.createRequire)(packageJson)(path19.join(path19.dirname(packageJson), "lib", "globals.js"));
7508
+ const globals = (0, import_node_module2.createRequire)(packageJson)(path20.join(path20.dirname(packageJson), "lib", "globals.js"));
7301
7509
  const read = globals.currentTestInfo;
7302
7510
  if (typeof read !== "function") return () => null;
7303
7511
  return () => {
@@ -7326,15 +7534,15 @@ function countHandles() {
7326
7534
  async function settledHandles(start) {
7327
7535
  const end = countHandles();
7328
7536
  if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
7329
- await new Promise((resolve9) => setTimeout(resolve9, 0));
7537
+ await new Promise((resolve11) => setTimeout(resolve11, 0));
7330
7538
  return countHandles();
7331
7539
  }
7332
7540
  function settleWithin(promise, ms) {
7333
7541
  let timer;
7334
7542
  return Promise.race([
7335
7543
  promise.catch(() => void 0),
7336
- new Promise((resolve9) => {
7337
- timer = setTimeout(resolve9, ms);
7544
+ new Promise((resolve11) => {
7545
+ timer = setTimeout(resolve11, ms);
7338
7546
  })
7339
7547
  ]).finally(() => clearTimeout(timer));
7340
7548
  }
@@ -7463,7 +7671,7 @@ var ResourceLedger = class {
7463
7671
  const census = this.census(null);
7464
7672
  if (this.resultsFile) {
7465
7673
  try {
7466
- fs20.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
7674
+ fs21.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
7467
7675
  `);
7468
7676
  } catch {
7469
7677
  }
@@ -7786,7 +7994,7 @@ async function recordResourceCensus(testInfo) {
7786
7994
  }
7787
7995
 
7788
7996
  // src/internal/collect/resource-verdicts.ts
7789
- var path20 = __toESM(require("path"));
7997
+ var path21 = __toESM(require("path"));
7790
7998
  var OPEN_STEP_TITLES = {
7791
7999
  "Create context": "context",
7792
8000
  "Launch persistent context": "context",
@@ -7818,7 +8026,7 @@ function userFixturesOf(steps) {
7818
8026
  const walk = (list) => {
7819
8027
  for (const step of list) {
7820
8028
  const file = step.location?.file;
7821
- if (step.category === "fixture" && file && !file.includes(`${path20.sep}node_modules${path20.sep}`)) {
8029
+ if (step.category === "fixture" && file && !file.includes(`${path21.sep}node_modules${path21.sep}`)) {
7822
8030
  const name = step.title.replace(/^(?:Fixture\s+|fixture:\s*)/, "").replace(/^"(.*)"$/, "$1");
7823
8031
  if (!name.startsWith("piwi")) out.push(`${name} (${relativeFile(file)}:${step.location.line})`);
7824
8032
  }
@@ -7851,7 +8059,7 @@ function tallyLifecycleSteps(file, steps) {
7851
8059
  return tally;
7852
8060
  }
7853
8061
  function relativeFile(file) {
7854
- return path20.relative(process.cwd(), file).split(path20.sep).join("/");
8062
+ return path21.relative(process.cwd(), file).split(path21.sep).join("/");
7855
8063
  }
7856
8064
  function buildResourceReport(input) {
7857
8065
  const findings = [];
@@ -8209,9 +8417,9 @@ function formatResourceSummary(report, leakCheck, maxLines = 10) {
8209
8417
  }
8210
8418
 
8211
8419
  // src/internal/collect/process-sampler.ts
8212
- var fs21 = __toESM(require("fs"));
8420
+ var fs22 = __toESM(require("fs"));
8213
8421
  var os8 = __toESM(require("os"));
8214
- var path21 = __toESM(require("path"));
8422
+ var path22 = __toESM(require("path"));
8215
8423
  var import_node_child_process2 = require("child_process");
8216
8424
  var WALK_FILE_CAP = 1e4;
8217
8425
  var SERIES_CAP = 3600;
@@ -8251,17 +8459,17 @@ function nodeCpuTicks() {
8251
8459
  return { busy, idle, iowait: 0, steal: 0, total: busy + idle };
8252
8460
  }
8253
8461
  function defaultPs() {
8254
- return new Promise((resolve9, reject) => {
8462
+ return new Promise((resolve11, reject) => {
8255
8463
  (0, import_node_child_process2.execFile)(
8256
8464
  "ps",
8257
8465
  ["-A", "-o", "pid=,ppid=,rss=,time=,command="],
8258
8466
  { timeout: 2e3, maxBuffer: 8 * 1024 * 1024 },
8259
- (error, stdout) => error ? reject(error) : resolve9(stdout)
8467
+ (error, stdout) => error ? reject(error) : resolve11(stdout)
8260
8468
  );
8261
8469
  });
8262
8470
  }
8263
8471
  function defaultWindowsQuery() {
8264
- return new Promise((resolve9, reject) => {
8472
+ return new Promise((resolve11, reject) => {
8265
8473
  (0, import_node_child_process2.execFile)(
8266
8474
  "powershell.exe",
8267
8475
  [
@@ -8271,18 +8479,18 @@ function defaultWindowsQuery() {
8271
8479
  "[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
8480
  ],
8273
8481
  { timeout: 1e4, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
8274
- (error, stdout) => error ? reject(error) : resolve9(stdout)
8482
+ (error, stdout) => error ? reject(error) : resolve11(stdout)
8275
8483
  );
8276
8484
  });
8277
8485
  }
8278
8486
  function freeBytes(dir) {
8279
- let current = path21.resolve(dir);
8487
+ let current = path22.resolve(dir);
8280
8488
  for (; ; ) {
8281
8489
  try {
8282
- const stats = fs21.statfsSync(current);
8490
+ const stats = fs22.statfsSync(current);
8283
8491
  return stats.bavail * stats.bsize;
8284
8492
  } catch {
8285
- const parent = path21.dirname(current);
8493
+ const parent = path22.dirname(current);
8286
8494
  if (parent === current) return null;
8287
8495
  current = parent;
8288
8496
  }
@@ -8292,24 +8500,30 @@ async function dirBytes(dir, budget) {
8292
8500
  let bytes = 0;
8293
8501
  let entries;
8294
8502
  try {
8295
- entries = await fs21.promises.readdir(dir, { withFileTypes: true });
8503
+ entries = await fs22.promises.readdir(dir, { withFileTypes: true });
8296
8504
  } catch {
8297
8505
  return 0;
8298
8506
  }
8299
8507
  for (const entry of entries) {
8300
8508
  if (budget.files <= 0) return bytes;
8301
- const full = path21.join(dir, entry.name);
8509
+ const full = path22.join(dir, entry.name);
8302
8510
  if (entry.isDirectory()) bytes += await dirBytes(full, budget);
8303
8511
  else if (entry.isFile()) {
8304
8512
  budget.files--;
8305
8513
  try {
8306
- bytes += (await fs21.promises.lstat(full)).size;
8514
+ bytes += (await fs22.promises.lstat(full)).size;
8307
8515
  } catch {
8308
8516
  }
8309
8517
  }
8310
8518
  }
8311
8519
  return bytes;
8312
8520
  }
8521
+ function memoryOf(container) {
8522
+ const totalMemory = os8.totalmem();
8523
+ const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
8524
+ const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
8525
+ return { memoryBytes: totalMemory, memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null };
8526
+ }
8313
8527
  var RunSampler = class {
8314
8528
  constructor(options = {}) {
8315
8529
  this.timer = null;
@@ -8343,7 +8557,7 @@ var RunSampler = class {
8343
8557
  this.platform = options.platform ?? process.platform;
8344
8558
  this.proc = new ProcFs(options.procRoot);
8345
8559
  this.cgroup = new CgroupFs(options.procRoot, options.cgroupRoot);
8346
- this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path21.resolve(dir)))];
8560
+ this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path22.resolve(dir)))];
8347
8561
  this.tmpDir = options.tmpDir ?? os8.tmpdir();
8348
8562
  this.intervalMs = options.intervalMs ?? 1e3;
8349
8563
  this.heavyEvery = Math.max(1, options.heavyEvery ?? 5);
@@ -8378,6 +8592,18 @@ var RunSampler = class {
8378
8592
  }
8379
8593
  return this.profile(await this.leftoverBytes());
8380
8594
  }
8595
+ /**
8596
+ * What a live run's memory is drawn against: the machine's memory, or its
8597
+ * container's limit when lower, read once, and how the run's memory is
8598
+ * measured so far.
8599
+ */
8600
+ liveMemory() {
8601
+ if (this.capacityBytes === void 0) {
8602
+ const machine = memoryOf(this.linux ? this.cgroup.read() : null);
8603
+ this.capacityBytes = machine.memoryLimitBytes ?? machine.memoryBytes;
8604
+ }
8605
+ return { capacityBytes: this.capacityBytes || null, kind: this.memoryKind };
8606
+ }
8381
8607
  /** The series read so far, each point at the time it was read. */
8382
8608
  timedSeries() {
8383
8609
  return {
@@ -8545,7 +8771,7 @@ var RunSampler = class {
8545
8771
  } catch {
8546
8772
  return;
8547
8773
  }
8548
- this.noteTree(list, (proc) => roleOf(path21.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
8774
+ this.noteTree(list, (proc) => roleOf(path22.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
8549
8775
  }
8550
8776
  async sampleWindowsTree(at) {
8551
8777
  let list;
@@ -8601,7 +8827,7 @@ var RunSampler = class {
8601
8827
  let bytes = 0;
8602
8828
  for (const dir of this.outputDirs) bytes += await dirBytes(dir, budget);
8603
8829
  for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs >= this.startedAt - 1e3)) {
8604
- bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
8830
+ bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
8605
8831
  }
8606
8832
  if (budget.files <= 0) this.peakInUseIsLowerBound = true;
8607
8833
  this.peakInUse = Math.max(this.peakInUse ?? 0, bytes);
@@ -8610,7 +8836,7 @@ var RunSampler = class {
8610
8836
  async playwrightTempEntries(keep) {
8611
8837
  let names;
8612
8838
  try {
8613
- names = await fs21.promises.readdir(this.tmpDir);
8839
+ names = await fs22.promises.readdir(this.tmpDir);
8614
8840
  } catch {
8615
8841
  return [];
8616
8842
  }
@@ -8618,7 +8844,7 @@ var RunSampler = class {
8618
8844
  for (const name of names) {
8619
8845
  if (!PLAYWRIGHT_TEMP.test(name)) continue;
8620
8846
  try {
8621
- if (keep((await fs21.promises.stat(path21.join(this.tmpDir, name))).mtimeMs)) out.push(name);
8847
+ if (keep((await fs22.promises.stat(path22.join(this.tmpDir, name))).mtimeMs)) out.push(name);
8622
8848
  } catch {
8623
8849
  }
8624
8850
  }
@@ -8629,7 +8855,7 @@ var RunSampler = class {
8629
8855
  const budget = { files: WALK_FILE_CAP };
8630
8856
  let bytes = 0;
8631
8857
  for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs < this.startedAt - 1e3)) {
8632
- bytes += await dirBytes(path21.join(this.tmpDir, name), budget);
8858
+ bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
8633
8859
  }
8634
8860
  return bytes;
8635
8861
  } catch {
@@ -8674,9 +8900,6 @@ var RunSampler = class {
8674
8900
  if (cpuPressure === null) notMeasured.push("CPU pressure");
8675
8901
  const container = this.linux ? this.cgroup.read() : null;
8676
8902
  const startContainer = this.containerStart;
8677
- const totalMemory = os8.totalmem();
8678
- const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
8679
- const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
8680
8903
  const raisedPeak = container?.memoryPeakBytes != null && startContainer?.memoryPeakBytes != null && container.memoryPeakBytes > startContainer.memoryPeakBytes ? container.memoryPeakBytes : null;
8681
8904
  const delta = (to, from) => to != null && from != null ? Math.max(0, to - from) : null;
8682
8905
  return {
@@ -8684,8 +8907,7 @@ var RunSampler = class {
8684
8907
  wallMs,
8685
8908
  machine: {
8686
8909
  cores: typeof os8.availableParallelism === "function" ? os8.availableParallelism() : os8.cpus().length,
8687
- memoryBytes: totalMemory,
8688
- memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null,
8910
+ ...memoryOf(container),
8689
8911
  cpuQuotaCores: container?.cpuQuotaCores ?? null
8690
8912
  },
8691
8913
  cpu: {
@@ -8957,7 +9179,7 @@ function openPagesByWorker(censuses) {
8957
9179
  }
8958
9180
  return out;
8959
9181
  }
8960
- function pagesOverTime(censuses) {
9182
+ function pagesOverTime(censuses, live = false) {
8961
9183
  const byWorker = /* @__PURE__ */ new Map();
8962
9184
  for (const census of censuses) {
8963
9185
  const list = byWorker.get(census.worker) ?? [];
@@ -8990,7 +9212,7 @@ function pagesOverTime(censuses) {
8990
9212
  if (last && last[0] === at) last[1] = open;
8991
9213
  else events.push([at, open]);
8992
9214
  }
8993
- if (open > 0 && lastAt >= events[events.length - 1][0]) events.push([lastAt, 0]);
9215
+ if (!live && open > 0 && lastAt >= events[events.length - 1][0]) events.push([lastAt, 0]);
8994
9216
  out.push({ worker, events });
8995
9217
  }
8996
9218
  return out;
@@ -9059,6 +9281,144 @@ function resourceReportWire(input) {
9059
9281
  };
9060
9282
  }
9061
9283
 
9284
+ // src/internal/streaming/live-resources.ts
9285
+ var LIVE_INTERVAL_MS = 5e3;
9286
+ function since(points, cursor, cap, merge) {
9287
+ const fresh = cursor === void 0 ? points : points.filter(([at]) => at > cursor);
9288
+ return downsamplePoints(fresh, cap, merge);
9289
+ }
9290
+ var LiveResources = class {
9291
+ constructor(stream, httpClient, options, logger = new Logger()) {
9292
+ this.stream = stream;
9293
+ this.httpClient = httpClient;
9294
+ this.options = options;
9295
+ this.logger = logger;
9296
+ this.timer = null;
9297
+ this.inFlight = null;
9298
+ this.stopped = false;
9299
+ /** Whether the next post must carry every point: the first one, and after the dashboard lost them. */
9300
+ this.reset = true;
9301
+ /** The time of the last point the dashboard took, per series. */
9302
+ this.sent = /* @__PURE__ */ new Map();
9303
+ /** How many censuses the open pages last posted came from. */
9304
+ this.censusCount = 0;
9305
+ }
9306
+ /** Post on a timer that never keeps the process alive. */
9307
+ start() {
9308
+ if (this.timer || this.stopped) return;
9309
+ this.timer = setInterval(() => this.tick(), this.options.intervalMs ?? LIVE_INTERVAL_MS);
9310
+ this.timer.unref?.();
9311
+ }
9312
+ /** Post nothing more, and wait for a post in flight. */
9313
+ async stop() {
9314
+ this.stopped = true;
9315
+ if (this.timer) clearInterval(this.timer);
9316
+ this.timer = null;
9317
+ await this.inFlight;
9318
+ }
9319
+ /** Post now, when the run is watched and no post is in flight: the dashboard says someone just opened it. */
9320
+ tick() {
9321
+ if (this.stopped || this.inFlight) return null;
9322
+ const { enabled, runId, token } = this.stream;
9323
+ if (!enabled || runId === null || !token || this.stream.isWatched !== true) return null;
9324
+ let draft;
9325
+ try {
9326
+ draft = this.draft();
9327
+ } catch (error) {
9328
+ this.logger.debug(`Live resources not read: ${errorMessage(error)}`);
9329
+ return null;
9330
+ }
9331
+ if (!draft) return null;
9332
+ this.inFlight = this.send(runId, token, draft).finally(() => {
9333
+ this.inFlight = null;
9334
+ });
9335
+ return this.inFlight;
9336
+ }
9337
+ /** What is new since the last post the dashboard took; null when nothing is. */
9338
+ draft() {
9339
+ const samples = this.options.sampler?.timedSeries() ?? null;
9340
+ const startedAt = samples?.startedAt || this.options.runStartedAt;
9341
+ const censuses = this.options.censuses();
9342
+ const cursors = this.reset ? /* @__PURE__ */ new Map() : this.sent;
9343
+ const sent = new Map(cursors);
9344
+ let fresh = false;
9345
+ const take = (key, points, cap, merge) => {
9346
+ const out = since(points, cursors.get(key), cap, merge);
9347
+ const last = points[points.length - 1];
9348
+ if (out.length > 0 && last) {
9349
+ sent.set(key, last[0]);
9350
+ fresh = true;
9351
+ }
9352
+ return out;
9353
+ };
9354
+ const timeline = { startedAt, cpuPct: [], memoryBytes: [], pages: [], workers: [] };
9355
+ if (samples) {
9356
+ timeline.cpuPct = take("cpu", samples.cpuPct, TIMELINE_SERIES_CAP, "mean");
9357
+ timeline.memoryBytes = take("memory", samples.memoryBytes, TIMELINE_SERIES_CAP, "max");
9358
+ for (const tree of workerTreesByIndex(samples, censuses)) {
9359
+ const cpuCores = take(`cores:${tree.worker}`, tree.cpuCores, TIMELINE_WORKER_POINTS_CAP, "mean");
9360
+ const memoryBytes = take(`bytes:${tree.worker}`, tree.memoryBytes, TIMELINE_WORKER_POINTS_CAP, "max");
9361
+ if (cpuCores.length > 0 || memoryBytes.length > 0) {
9362
+ timeline.workers.push({ worker: tree.worker, cpuCores, memoryBytes });
9363
+ }
9364
+ }
9365
+ }
9366
+ if (this.reset || censuses.length !== this.censusCount) {
9367
+ for (const { worker, events } of pagesOverTime(censuses, true)) {
9368
+ const points = events.map(([at, open]) => [at - startedAt, open]);
9369
+ const last = points.pop();
9370
+ timeline.pages.push({
9371
+ worker,
9372
+ points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last]
9373
+ });
9374
+ }
9375
+ fresh ||= timeline.pages.length > 0;
9376
+ }
9377
+ if (!fresh) return null;
9378
+ const memory = this.options.sampler?.liveMemory() ?? { capacityBytes: null, kind: null };
9379
+ return {
9380
+ post: {
9381
+ shardIndex: this.options.shardIndex,
9382
+ reset: this.reset,
9383
+ timeline,
9384
+ memoryCapacityBytes: memory.capacityBytes,
9385
+ memoryKind: memory.kind
9386
+ },
9387
+ sent,
9388
+ censusCount: censuses.length
9389
+ };
9390
+ }
9391
+ async send(runId, token, draft) {
9392
+ try {
9393
+ const answer = await this.httpClient.postJSON(
9394
+ `/api/test-runs/${runId}/live-resources`,
9395
+ { streamToken: token, ...draft.post },
9396
+ this.stream.auth
9397
+ );
9398
+ if (answer?.declined) {
9399
+ this.logger.debug("The project declined resources: live resources are not sent.");
9400
+ this.stopped = true;
9401
+ return;
9402
+ }
9403
+ this.stream.noteWatched(answer);
9404
+ if (answer?.resync) {
9405
+ this.reset = true;
9406
+ return;
9407
+ }
9408
+ this.sent = draft.sent;
9409
+ this.censusCount = draft.censusCount;
9410
+ this.reset = false;
9411
+ } catch (error) {
9412
+ if (error instanceof HttpError && error.status === 404) {
9413
+ this.logger.debug("The dashboard takes no live resources.");
9414
+ this.stopped = true;
9415
+ return;
9416
+ }
9417
+ this.logger.debug(`Live resources not sent: ${errorMessage(error)}`);
9418
+ }
9419
+ }
9420
+ };
9421
+
9062
9422
  // ../core/src/describe-failure.ts
9063
9423
  var HEADLINE_MAX_CHARS = 120;
9064
9424
  var VALUE_MAX_CHARS = 40;
@@ -9588,10 +9948,14 @@ function isServerGranularity(value) {
9588
9948
  return typeof value === "string" && SERVER_VALUES.has(value);
9589
9949
  }
9590
9950
  function coverageSettings(options) {
9591
- if (options.collectPerformanceMetrics === false) return { browser: "off", server: "off" };
9951
+ if (options.collectPerformanceMetrics === false) return { browser: "off", server: "off", css: "off" };
9592
9952
  const browser = options.coverage?.browser === "functions" ? "functions" : options.coverage?.browser === "files" || options.captureCodeReach === true ? "files" : "off";
9593
9953
  const server = isServerGranularity(options.coverage?.server) ? options.coverage.server : "off";
9594
- return { browser, server };
9954
+ const css = options.coverage?.css === "rules" ? "rules" : "off";
9955
+ return { browser, server, css };
9956
+ }
9957
+ function cssCoverageInWorker() {
9958
+ return process.env[PIWI_ENV_KEYS.coverageCss] === "rules";
9595
9959
  }
9596
9960
  function browserCoverageInWorker() {
9597
9961
  const browser = process.env[PIWI_ENV_KEYS.coverageBrowser];
@@ -9603,7 +9967,7 @@ function browserFunctionsInWorker() {
9603
9967
  function browserFunctionCoverage(files) {
9604
9968
  return sanitizeCoverage({
9605
9969
  v: 1,
9606
- files: [...files].map(([path32, functions]) => ({ path: path32, runtime: "browser", functions }))
9970
+ files: [...files].map(([path34, functions]) => ({ path: path34, runtime: "browser", functions }))
9607
9971
  });
9608
9972
  }
9609
9973
  function newKnownFunctions(known, sent) {
@@ -9616,19 +9980,19 @@ function newKnownFunctions(known, sent) {
9616
9980
  budget = MAX_COVERAGE_RANGES;
9617
9981
  };
9618
9982
  for (const [raw, ranges] of known) {
9619
- const path32 = normalizeCoveragePath(raw);
9620
- if (path32 === null) continue;
9621
- let named = sent.get(path32);
9983
+ const path34 = normalizeCoveragePath(raw);
9984
+ if (path34 === null) continue;
9985
+ let named = sent.get(path34);
9622
9986
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
9623
9987
  if (fresh.length === 0) continue;
9624
- if (!named) sent.set(path32, named = /* @__PURE__ */ new Set());
9988
+ if (!named) sent.set(path34, named = /* @__PURE__ */ new Set());
9625
9989
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
9626
9990
  while (fresh.length > 0) {
9627
9991
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
9628
9992
  const part = fresh.slice(0, budget);
9629
9993
  fresh = fresh.slice(part.length);
9630
9994
  budget -= part.length;
9631
- files.push({ path: path32, allFunctions: part });
9995
+ files.push({ path: path34, allFunctions: part });
9632
9996
  }
9633
9997
  }
9634
9998
  close();
@@ -9640,8 +10004,8 @@ function serverCoverageInWorker() {
9640
10004
 
9641
10005
  // src/internal/capture/code-reach.ts
9642
10006
  var crypto5 = __toESM(require("crypto"));
9643
- var fs22 = __toESM(require("fs"));
9644
- var path22 = __toESM(require("path"));
10007
+ var fs23 = __toESM(require("fs"));
10008
+ var path23 = __toESM(require("path"));
9645
10009
  var import_node_child_process3 = require("child_process");
9646
10010
 
9647
10011
  // ../core/src/source-map.ts
@@ -9711,10 +10075,10 @@ function joinRoot(root, source) {
9711
10075
  function decodeLines(raw) {
9712
10076
  const sources = [];
9713
10077
  const decoded = [];
9714
- const add = (map, lineOffset, columnOffset) => {
10078
+ const add2 = (map, lineOffset, columnOffset) => {
9715
10079
  if (map.sections) {
9716
10080
  for (const section of map.sections) {
9717
- if (!add(section.map, section.offset.line, section.offset.column)) return false;
10081
+ if (!add2(section.map, section.offset.line, section.offset.column)) return false;
9718
10082
  }
9719
10083
  return true;
9720
10084
  }
@@ -9735,7 +10099,7 @@ function decodeLines(raw) {
9735
10099
  });
9736
10100
  return true;
9737
10101
  };
9738
- return add(raw, 0, 0) ? { sources, lines: decoded } : null;
10102
+ return add2(raw, 0, 0) ? { sources, lines: decoded } : null;
9739
10103
  }
9740
10104
  function compactSegments(lines) {
9741
10105
  let total = 0;
@@ -9911,7 +10275,7 @@ function functionLines(entry, map, text, all) {
9911
10275
  const read = textOf(entry, text);
9912
10276
  const ranges = executedFunctionRanges(entry, read?.length, all);
9913
10277
  if (!ranges.length || !read) return out;
9914
- const add = (source, range) => {
10278
+ const add2 = (source, range) => {
9915
10279
  const list = out.get(source);
9916
10280
  if (list) list.push(range);
9917
10281
  else out.set(source, [range]);
@@ -9921,7 +10285,7 @@ function functionLines(entry, map, text, all) {
9921
10285
  const start = offsetToPosition(starts, range.start);
9922
10286
  if (!map) {
9923
10287
  const last = offsetToPosition(starts, Math.max(range.start, range.end - 1));
9924
- add(-1, [start.line + 1, last.line + 1]);
10288
+ add2(-1, [start.line + 1, last.line + 1]);
9925
10289
  continue;
9926
10290
  }
9927
10291
  const at = map.originalAt(start.line, start.column);
@@ -9931,7 +10295,7 @@ function functionLines(entry, map, text, all) {
9931
10295
  if (first === null) continue;
9932
10296
  const header = first === at.line || first === at.line + 1 ? at.line : first;
9933
10297
  const lines = map.originalLinesBetween(at.source, start, end, header);
9934
- if (lines) add(at.source, [header + 1, lines[1] + 1]);
10298
+ if (lines) add2(at.source, [header + 1, lines[1] + 1]);
9935
10299
  }
9936
10300
  return out;
9937
10301
  }
@@ -9986,18 +10350,18 @@ function repositoryRoot(from) {
9986
10350
  }).trim();
9987
10351
  } catch {
9988
10352
  }
9989
- cachedRepoRoot = { from, root: path22.resolve(root) };
10353
+ cachedRepoRoot = { from, root: path23.resolve(root) };
9990
10354
  return cachedRepoRoot.root;
9991
10355
  }
9992
10356
  function codeReachRoots(configDir, configured) {
9993
10357
  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?.["*"]);
10358
+ const resolve11 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path23.resolve(configDir, r));
10359
+ const global = Array.isArray(configured) ? resolve11(configured) : resolve11(configured?.["*"]);
9996
10360
  const byOrigin = /* @__PURE__ */ new Map();
9997
10361
  if (configured && !Array.isArray(configured)) {
9998
10362
  for (const [key, dirs] of Object.entries(configured)) {
9999
10363
  const origin = key === "*" ? null : originOf3(key);
10000
- const resolved = resolve9(dirs);
10364
+ const resolved = resolve11(dirs);
10001
10365
  if (origin && resolved.length) byOrigin.set(origin, resolved);
10002
10366
  }
10003
10367
  }
@@ -10020,7 +10384,7 @@ function scriptRoots(roots, scriptUrl, pageUrl) {
10020
10384
  }
10021
10385
  function isFile(file) {
10022
10386
  try {
10023
- return fs22.statSync(file).isFile();
10387
+ return fs23.statSync(file).isFile();
10024
10388
  } catch {
10025
10389
  return false;
10026
10390
  }
@@ -10028,10 +10392,10 @@ function isFile(file) {
10028
10392
  function resolveSourcePath(source, roots) {
10029
10393
  const normalized = normalizeSourcePath(source);
10030
10394
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
10031
- if (path22.isAbsolute(normalized) && isFile(normalized)) return path22.resolve(normalized);
10395
+ if (path23.isAbsolute(normalized) && isFile(normalized)) return path23.resolve(normalized);
10032
10396
  const relative11 = normalized.replace(/^\/+/, "");
10033
10397
  for (const root of roots) {
10034
- const candidate = path22.resolve(root, relative11);
10398
+ const candidate = path23.resolve(root, relative11);
10035
10399
  if (isFile(candidate)) return candidate;
10036
10400
  }
10037
10401
  return null;
@@ -10063,7 +10427,7 @@ function fileForUrl(url, roots, bases) {
10063
10427
  return file;
10064
10428
  }
10065
10429
  function relativeToRepo(file, repoRoot) {
10066
- return path22.relative(repoRoot, file).split(path22.sep).join("/");
10430
+ return path23.relative(repoRoot, file).split(path23.sep).join("/");
10067
10431
  }
10068
10432
  function scriptVersion(entry) {
10069
10433
  return entry.hash ?? crypto5.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -10120,12 +10484,12 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
10120
10484
  const files = /* @__PURE__ */ new Map();
10121
10485
  const known = /* @__PURE__ */ new Map();
10122
10486
  const addTo = (into, file, functions) => {
10123
- const path32 = relativeToRepo(file, roots.repoRoot);
10124
- const listed = into.get(path32);
10487
+ const path34 = relativeToRepo(file, roots.repoRoot);
10488
+ const listed = into.get(path34);
10125
10489
  if (listed) listed.push(...functions);
10126
- else into.set(path32, [...functions]);
10490
+ else into.set(path34, [...functions]);
10127
10491
  };
10128
- const add = (file, functions) => addTo(files, file, functions);
10492
+ const add2 = (file, functions) => addTo(files, file, functions);
10129
10493
  const bases = viteBases(entries);
10130
10494
  const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
10131
10495
  const cached = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
@@ -10152,14 +10516,14 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
10152
10516
  const text = textOf2(covered, resolution);
10153
10517
  if (resolution.lookup) {
10154
10518
  const reached = reachedFunctionLines(covered, resolution.lookup, text);
10155
- if (reached.size > 0) add(resolution.file, [...reached.values()].flat());
10519
+ if (reached.size > 0) add2(resolution.file, [...reached.values()].flat());
10156
10520
  } else if (scriptRan(covered, text)) {
10157
- add(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
10521
+ add2(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
10158
10522
  }
10159
10523
  } else if (resolution.kind === "map") {
10160
10524
  for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup, textOf2(covered, resolution))) {
10161
10525
  const file = mapFile(resolution, index);
10162
- if (file) add(file, functions);
10526
+ if (file) add2(file, functions);
10163
10527
  }
10164
10528
  }
10165
10529
  if (!withKnown || resolution.kind === "none") continue;
@@ -10238,10 +10602,13 @@ async function markCurrentDocument(page, cdp) {
10238
10602
  }
10239
10603
  function onPaused(page, capture, event) {
10240
10604
  const leaving = event.reason === "EventListener" && event.data?.eventName === "listener:beforeunload" && (event.callFrames?.[0]?.functionName ?? MARK_FUNCTION) === MARK_FUNCTION;
10241
- const read = leaving ? readCoverage(page, capture.cdp) : null;
10605
+ const read = leaving ? Promise.all([
10606
+ capture.js ? readCoverage(page, capture.cdp) : Promise.resolve([]),
10607
+ capture.css ? readCssUsage(page, capture.cdp, capture.css) : Promise.resolve()
10608
+ ]) : null;
10242
10609
  if (read) {
10243
10610
  const merging = read.then(
10244
- (coverage) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
10611
+ ([coverage]) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
10245
10612
  );
10246
10613
  capture.reading.add(merging);
10247
10614
  void merging.finally(() => capture.reading.delete(merging));
@@ -10255,7 +10622,7 @@ function versionKey(url, hash) {
10255
10622
  return `${hash} ${url}`;
10256
10623
  }
10257
10624
  function onScriptParsed(page, capture, event) {
10258
- if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
10625
+ if (!capture.js || !event.url || REPLAYED_SCRIPT.test(event.url)) return;
10259
10626
  capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash, pageUrl: capture.pageUrl });
10260
10627
  if (NEVER_REACHED.test(event.url)) return;
10261
10628
  const key = versionKey(event.url, event.hash);
@@ -10275,8 +10642,8 @@ function onScriptParsed(page, capture, event) {
10275
10642
  var READ_WAIT_MS = 5e3;
10276
10643
  function readCoverage(page, cdp) {
10277
10644
  let timer;
10278
- const late = new Promise((resolve9) => {
10279
- timer = setTimeout(() => resolve9([]), READ_WAIT_MS);
10645
+ const late = new Promise((resolve11) => {
10646
+ timer = setTimeout(() => resolve11([]), READ_WAIT_MS);
10280
10647
  });
10281
10648
  const taken = internalCall(page, () => cdp.send("Profiler.takePreciseCoverage")).then(
10282
10649
  (answer) => answer.result,
@@ -10284,8 +10651,55 @@ function readCoverage(page, cdp) {
10284
10651
  );
10285
10652
  return Promise.race([taken, late]).finally(() => clearTimeout(timer));
10286
10653
  }
10287
- function mergeCoverageRead(ran, scripts, read, keepIdle = true) {
10288
- for (const covered of read) {
10654
+ function onStyleSheetAdded(page, cdp, css, pageUrl, header) {
10655
+ if (header.isConstructed) return;
10656
+ const sheet = {
10657
+ sourceURL: header.sourceURL,
10658
+ ...header.sourceMapURL ? { sourceMapURL: header.sourceMapURL } : {},
10659
+ isInline: header.isInline,
10660
+ startLine: header.startLine,
10661
+ pageUrl,
10662
+ used: /* @__PURE__ */ new Map()
10663
+ };
10664
+ css.sheets.set(header.styleSheetId, sheet);
10665
+ const reading = (async () => {
10666
+ try {
10667
+ if (header.ownerNode !== void 0 && !/\.css(?:[?#]|$)/i.test(header.sourceURL)) {
10668
+ const { node } = await internalCall(
10669
+ page,
10670
+ () => cdp.send("DOM.describeNode", { backendNodeId: header.ownerNode })
10671
+ );
10672
+ const attributes = node.attributes ?? [];
10673
+ const at = attributes.indexOf("data-vite-dev-id");
10674
+ if (at >= 0 && typeof attributes[at + 1] === "string") sheet.devId = attributes[at + 1];
10675
+ }
10676
+ if (/\/node_modules\//.test(sheet.devId ?? header.sourceURL)) return;
10677
+ const { text } = await internalCall(
10678
+ page,
10679
+ () => cdp.send("CSS.getStyleSheetText", { styleSheetId: header.styleSheetId })
10680
+ );
10681
+ sheet.text = text;
10682
+ } catch {
10683
+ }
10684
+ })();
10685
+ css.reading.add(reading);
10686
+ void reading.finally(() => css.reading.delete(reading));
10687
+ }
10688
+ async function readCssUsage(page, cdp, css) {
10689
+ try {
10690
+ const { coverage } = await internalCall(page, () => cdp.send("CSS.takeCoverageDelta"));
10691
+ for (const rule of coverage) {
10692
+ if (!rule.used) continue;
10693
+ css.sheets.get(rule.styleSheetId)?.used.set(`${rule.startOffset}:${rule.endOffset}`, {
10694
+ startOffset: rule.startOffset,
10695
+ endOffset: rule.endOffset
10696
+ });
10697
+ }
10698
+ } catch {
10699
+ }
10700
+ }
10701
+ function mergeCoverageRead(ran, scripts, read, keepIdle = true) {
10702
+ for (const covered of read) {
10289
10703
  const script = scripts.get(covered.scriptId);
10290
10704
  if (!script) continue;
10291
10705
  const key = versionKey(script.url, script.hash);
@@ -10318,11 +10732,15 @@ async function detach(page, cdp) {
10318
10732
  }
10319
10733
  async function startCodeReach(page) {
10320
10734
  if (!isChromiumPage(page) || captures.has(page)) return false;
10735
+ const withCss = cssCoverageInWorker();
10736
+ const js = browserCoverageInWorker() || !withCss;
10321
10737
  let cdp = null;
10322
10738
  try {
10323
10739
  cdp = await internalCall(page, () => page.context().newCDPSession(page));
10324
10740
  const capture = {
10325
10741
  cdp,
10742
+ js,
10743
+ css: withCss ? { sheets: /* @__PURE__ */ new Map(), reading: /* @__PURE__ */ new Set() } : null,
10326
10744
  scripts: /* @__PURE__ */ new Map(),
10327
10745
  sources: /* @__PURE__ */ new Map(),
10328
10746
  ran: /* @__PURE__ */ new Map(),
@@ -10332,6 +10750,9 @@ async function startCodeReach(page) {
10332
10750
  };
10333
10751
  const marks = marksOf(page);
10334
10752
  cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
10753
+ const css = capture.css;
10754
+ if (css)
10755
+ cdp.on("CSS.styleSheetAdded", (event) => onStyleSheetAdded(page, cdp, css, capture.pageUrl, event.header));
10335
10756
  cdp.on("Debugger.paused", (event) => onPaused(page, capture, event));
10336
10757
  cdp.on("Page.frameNavigated", (event) => {
10337
10758
  if (event.frame.parentId) return;
@@ -10342,8 +10763,17 @@ async function startCodeReach(page) {
10342
10763
  page,
10343
10764
  () => Promise.all([
10344
10765
  capture.cdp.send("Debugger.enable"),
10345
- capture.cdp.send("Profiler.enable"),
10346
- capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false }),
10766
+ ...js ? [
10767
+ capture.cdp.send("Profiler.enable"),
10768
+ capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
10769
+ ] : [],
10770
+ // The style sheets' rule usage records nothing without the DOM and CSS domains. A failure there leaves the
10771
+ // scripts' coverage running.
10772
+ ...css ? [
10773
+ capture.cdp.send("DOM.enable").then(() => capture.cdp.send("CSS.enable")).then(() => capture.cdp.send("CSS.startRuleUsageTracking")).catch(() => {
10774
+ capture.css = null;
10775
+ })
10776
+ ] : [],
10347
10777
  capture.cdp.send("Page.enable"),
10348
10778
  capture.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: MARK_SOURCE, worldName: MARK_WORLD }),
10349
10779
  capture.cdp.send("DOMDebugger.setEventListenerBreakpoint", { eventName: "beforeunload" })
@@ -10357,18 +10787,26 @@ async function startCodeReach(page) {
10357
10787
  return false;
10358
10788
  }
10359
10789
  }
10360
- async function stopCodeReach(page) {
10790
+ async function stopPageCoverage(page) {
10361
10791
  const capture = captures.get(page);
10362
- if (!capture) return null;
10792
+ if (!capture) return { scripts: null, sheets: [] };
10363
10793
  captures.delete(page);
10364
10794
  const { cdp } = capture;
10365
- try {
10795
+ const sheets = async () => {
10796
+ if (!capture.css) return [];
10797
+ await readCssUsage(page, cdp, capture.css);
10798
+ await Promise.all(capture.css.reading);
10799
+ await internalCall(page, () => cdp.send("CSS.stopRuleUsageTracking")).catch(() => {
10800
+ });
10801
+ return [...capture.css.sheets.values()].filter((sheet) => sheet.used.size > 0 || sheet.text !== void 0);
10802
+ };
10803
+ const scripts = async () => {
10804
+ if (!capture.js) return null;
10366
10805
  const last = await internalCall(page, async () => {
10367
10806
  const taken = await cdp.send("Profiler.takePreciseCoverage");
10368
10807
  await cdp.send("Profiler.stopPreciseCoverage");
10369
10808
  return taken.result;
10370
10809
  });
10371
- await Promise.all(capture.reading);
10372
10810
  mergeCoverageRead(capture.ran, capture.scripts, last, capture.keepIdle);
10373
10811
  const entries = [];
10374
10812
  for (const [key, { script, functions }] of capture.ran) {
@@ -10378,8 +10816,11 @@ async function stopCodeReach(page) {
10378
10816
  entries.push(entry);
10379
10817
  }
10380
10818
  return entries;
10381
- } catch {
10382
- return null;
10819
+ };
10820
+ try {
10821
+ await Promise.all(capture.reading);
10822
+ const [read, styles] = await Promise.all([scripts().catch(() => null), sheets().catch(() => [])]);
10823
+ return { scripts: read, sheets: styles };
10383
10824
  } finally {
10384
10825
  await detach(page, cdp);
10385
10826
  }
@@ -10388,8 +10829,8 @@ async function stopCodeReach(page) {
10388
10829
  // src/internal/collect/blob-ids.ts
10389
10830
  var import_node_child_process4 = require("child_process");
10390
10831
  var crypto6 = __toESM(require("crypto"));
10391
- var fs23 = __toESM(require("fs"));
10392
- var path23 = __toESM(require("path"));
10832
+ var fs24 = __toESM(require("fs"));
10833
+ var path24 = __toESM(require("path"));
10393
10834
  function gitBlobId(content) {
10394
10835
  return crypto6.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
10395
10836
  }
@@ -10398,14 +10839,14 @@ function namesLines(file) {
10398
10839
  }
10399
10840
  var GIT_BATCH = 500;
10400
10841
  function gitHashObjects(root, paths) {
10401
- return new Promise((resolve9) => {
10842
+ return new Promise((resolve11) => {
10402
10843
  const child = (0, import_node_child_process4.execFile)(
10403
10844
  "git",
10404
10845
  ["hash-object", "--stdin-paths"],
10405
10846
  { cwd: root, encoding: "utf8", timeout: 3e4, windowsHide: true },
10406
10847
  (error, stdout) => {
10407
10848
  const ids = error ? [] : stdout.split("\n").filter(Boolean);
10408
- resolve9(ids.length === paths.length ? ids : null);
10849
+ resolve11(ids.length === paths.length ? ids : null);
10409
10850
  }
10410
10851
  );
10411
10852
  child.stdin?.on("error", () => {
@@ -10456,15 +10897,15 @@ var BlobIds = class {
10456
10897
  async read(relativePaths) {
10457
10898
  let root;
10458
10899
  try {
10459
- root = path23.resolve(this.repoRoot());
10900
+ root = path24.resolve(this.repoRoot());
10460
10901
  } catch {
10461
10902
  return relativePaths.map(() => null);
10462
10903
  }
10463
10904
  const files = await Promise.all(
10464
10905
  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);
10906
+ const file = path24.resolve(root, relativePath2);
10907
+ if (!file.startsWith(root + path24.sep)) return null;
10908
+ const stat = await fs24.promises.stat(file).catch(() => null);
10468
10909
  return stat?.isFile() ? file : null;
10469
10910
  })
10470
10911
  );
@@ -10472,7 +10913,7 @@ var BlobIds = class {
10472
10913
  const fromGit = /* @__PURE__ */ new Map();
10473
10914
  const ids = forGit.length > 0 ? await this.hashObjects(
10474
10915
  root,
10475
- forGit.map((file) => path23.relative(root, file))
10916
+ forGit.map((file) => path24.relative(root, file))
10476
10917
  ) : null;
10477
10918
  if (ids) forGit.forEach((file, i) => fromGit.set(file, ids[i]));
10478
10919
  return Promise.all(
@@ -10481,7 +10922,7 @@ var BlobIds = class {
10481
10922
  const id = fromGit.get(file);
10482
10923
  if (id) return id;
10483
10924
  try {
10484
- return gitBlobId(await fs23.promises.readFile(file));
10925
+ return gitBlobId(await fs24.promises.readFile(file));
10485
10926
  } catch {
10486
10927
  return null;
10487
10928
  }
@@ -10496,7 +10937,7 @@ var SETTLE_WAIT_MS = 15e3;
10496
10937
  var RUN_COVERAGE_FILES_PER_REQUEST = 5e3;
10497
10938
  var RUN_COVERAGE_BYTES_PER_REQUEST = 8 * 1024 * 1024;
10498
10939
  var INCOMPLETE = { files: [], incomplete: true };
10499
- function composeTestCoverage(server, browserFiles, settings) {
10940
+ function composeTestCoverage(server, browserFiles, settings, cssFiles) {
10500
10941
  const serverCapture = settings.server !== "off" && server ? reduceCoverage(
10501
10942
  {
10502
10943
  v: 1,
@@ -10514,7 +10955,19 @@ function composeTestCoverage(server, browserFiles, settings) {
10514
10955
  },
10515
10956
  settings.browser
10516
10957
  ) : null;
10517
- const merged = mergeCoverage(serverCapture, browserCapture);
10958
+ const wholeFiles = new Set(
10959
+ settings.browser === "functions" ? [] : (browserFiles ?? []).map((file) => typeof file === "string" ? file : file.path)
10960
+ );
10961
+ const cssCapture = settings.css === "rules" && cssFiles?.length ? reduceCoverage(
10962
+ {
10963
+ v: 1,
10964
+ files: cssFiles.map(
10965
+ (file) => wholeFiles.has(file.path) ? { path: file.path, runtime: "browser" } : { ...file, runtime: "browser" }
10966
+ )
10967
+ },
10968
+ "functions"
10969
+ ) : null;
10970
+ const merged = mergeCoverage(serverCapture, mergeCoverage(browserCapture, cssCapture));
10518
10971
  return merged ? sanitizeCoverage(merged) : null;
10519
10972
  }
10520
10973
  function addRanges(into, filePath2, ranges) {
@@ -10574,7 +11027,7 @@ var CoverageCollection = class {
10574
11027
  this.browserKnown = /* @__PURE__ */ new Map();
10575
11028
  this.logger = options.logger ?? new Logger();
10576
11029
  this.waitMs = options.waitMs ?? TEST_COVERAGE_WAIT_MS;
10577
- this.browserBlobs = options.settings.browser === "functions" ? new BlobIds(() => repositoryRoot(options.configDir)) : null;
11030
+ this.browserBlobs = options.settings.browser === "functions" || options.settings.css === "rules" ? new BlobIds(() => repositoryRoot(options.configDir)) : null;
10578
11031
  const granularity = options.settings.server;
10579
11032
  this.collector = granularity === "off" ? null : new ServerCoverageCollector({
10580
11033
  granularity,
@@ -10591,28 +11044,40 @@ var CoverageCollection = class {
10591
11044
  /**
10592
11045
  * The browser side of a test's coverage: its code reach paths, or, at
10593
11046
  * `functions` granularity, the files of its `piwi-browser-coverage` attachment
10594
- * with their blob ids, once read. Never rejects.
11047
+ * with their blob ids, once read; with `coverage.css: 'rules'`, the style
11048
+ * sheet files of its `piwi-css-coverage` attachment too. Never rejects.
10595
11049
  */
10596
11050
  browserCoverage(testCase, attachments) {
10597
- if (this.options.settings.browser === "off") return void 0;
10598
- const paths = testCase.codeReach;
10599
- const attachment = this.browserBlobs ? attachments?.find((a) => a.name === ATTACHMENT_NAMES.browserCoverage) : void 0;
10600
- if (this.browserBlobs) {
10601
- for (const known of attachments ?? []) {
10602
- if (known.name === ATTACHMENT_NAMES.browserFunctions) this.addKnownFunctions(known);
10603
- }
10604
- }
10605
- if (!this.browserBlobs || !attachment?.body) return paths;
11051
+ const { browser, css } = this.options.settings;
11052
+ if (browser === "off" && css !== "rules") return {};
11053
+ const paths = browser === "off" ? void 0 : testCase.codeReach;
11054
+ for (const known of attachments ?? []) {
11055
+ if (browser === "functions" && known.name === ATTACHMENT_NAMES.browserFunctions) this.addKnownFunctions(known);
11056
+ if (css === "rules" && known.name === ATTACHMENT_NAMES.cssRules) this.addKnownFunctions(known);
11057
+ }
11058
+ const scripts = browser === "functions" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.browserCoverage) : [];
11059
+ const sheets = css === "rules" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.cssCoverage) : [];
11060
+ const blobs = this.browserBlobs;
11061
+ if (!blobs || scripts.length === 0 && sheets.length === 0) return { files: paths };
11062
+ const withBlobs = (files) => blobs.withBlobs(files).catch(() => files);
11063
+ return Promise.all([
11064
+ scripts.length > 0 ? withBlobs(scripts) : Promise.resolve(null),
11065
+ sheets.length > 0 ? withBlobs(sheets) : Promise.resolve([])
11066
+ ]).then(([files, rules]) => ({ files: files ?? paths, css: rules }));
11067
+ }
11068
+ /** The browser files of a test's attachment holding line ranges, added to the run's. */
11069
+ rangedFiles(attachments, name) {
11070
+ const attachment = attachments?.find((a) => a.name === name);
11071
+ if (!attachment?.body) return [];
10606
11072
  let files = [];
10607
11073
  try {
10608
11074
  files = sanitizeCoverage(JSON.parse(attachment.body.toString("utf8")))?.files ?? [];
10609
11075
  } catch {
10610
- return paths;
11076
+ return [];
10611
11077
  }
10612
11078
  const browser = files.filter((file) => file.runtime === "browser");
10613
- if (browser.length === 0) return paths;
10614
11079
  for (const file of browser) addRanges(this.browserRan, file.path, file.functions ?? []);
10615
- return this.browserBlobs.withBlobs(browser).catch(() => browser);
11080
+ return browser;
10616
11081
  }
10617
11082
  /** Add the functions of a `piwi-browser-functions` attachment, one of a test's, to the run's. */
10618
11083
  addKnownFunctions(attachment) {
@@ -10667,27 +11132,27 @@ var CoverageCollection = class {
10667
11132
  const requests = attachment?.body ? parseServerCoverageRequests(attachment.body) : null;
10668
11133
  if (!this.collector || !requests) {
10669
11134
  if (!(browser instanceof Promise)) {
10670
- this.assign(testCase, composeTestCoverage(null, browser, this.options.settings));
11135
+ this.assign(testCase, composeTestCoverage(null, browser.files, this.options.settings, browser.css));
10671
11136
  return null;
10672
11137
  }
10673
11138
  const ready = browser.then(
10674
- (files) => this.assign(testCase, composeTestCoverage(null, files, this.options.settings))
11139
+ (side) => this.assign(testCase, composeTestCoverage(null, side.files, this.options.settings, side.css))
10675
11140
  );
10676
11141
  this.browserPending.add(ready);
10677
11142
  return ready.finally(() => this.browserPending.delete(ready));
10678
11143
  }
10679
11144
  let timer;
10680
11145
  let finish;
10681
- const done = new Promise((resolve9) => {
11146
+ const done = new Promise((resolve11) => {
10682
11147
  let settled = false;
10683
11148
  finish = (server) => {
10684
11149
  if (settled) return;
10685
11150
  settled = true;
10686
11151
  clearTimeout(timer);
10687
11152
  this.pending.delete(entry);
10688
- void Promise.resolve(browser).then((files) => {
10689
- this.assign(testCase, composeTestCoverage(server, files, this.options.settings));
10690
- resolve9();
11153
+ void Promise.resolve(browser).then((side) => {
11154
+ this.assign(testCase, composeTestCoverage(server, side.files, this.options.settings, side.css));
11155
+ resolve11();
10691
11156
  });
10692
11157
  };
10693
11158
  });
@@ -10719,8 +11184,8 @@ var CoverageCollection = class {
10719
11184
  let timer;
10720
11185
  await Promise.race([
10721
11186
  Promise.all(waiting),
10722
- new Promise((resolve9) => {
10723
- timer = setTimeout(resolve9, boundMs);
11187
+ new Promise((resolve11) => {
11188
+ timer = setTimeout(resolve11, boundMs);
10724
11189
  })
10725
11190
  ]);
10726
11191
  clearTimeout(timer);
@@ -10823,6 +11288,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10823
11288
  this.resourcesFile = null;
10824
11289
  /** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
10825
11290
  this.sampler = null;
11291
+ /** Posts what the sampler and the censuses add while someone has the run open on the dashboard. */
11292
+ this.liveResources = null;
10826
11293
  /** Bytes of the files the tests attached, by kind. */
10827
11294
  this.artifactBytes = emptyArtifacts();
10828
11295
  /** The run's resource report for the dashboard, built in `onEnd`. */
@@ -10838,10 +11305,13 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10838
11305
  this.streamManager = null;
10839
11306
  /** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
10840
11307
  this.setupSteps = [];
10841
- this.options = resolveOptions(rawOptions);
11308
+ const configDir = typeof rawOptions.configDir === "string" ? rawOptions.configDir : void 0;
11309
+ this.options = resolveOptions(rawOptions, configDir);
10842
11310
  this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
10843
11311
  this.listMode = isListMode();
10844
11312
  this.viaDesktopApp = usedDesktopDiscovery();
11313
+ this.desktopReplaced = desktopReplacedServer();
11314
+ this.viaDotEnv = usedDotEnv();
10845
11315
  this.coverageSettings = coverageSettings(this.options);
10846
11316
  const logger = new Logger(this.options.verbose ?? false);
10847
11317
  this.logger = logger;
@@ -10899,14 +11369,17 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10899
11369
  return;
10900
11370
  }
10901
11371
  if (this.viaDesktopApp) {
10902
- this.logger.info(`Using the desktop app running at ${this.options.serverUrl} (no serverUrl configured).`);
11372
+ this.logger.info(
11373
+ 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).`
11374
+ );
10903
11375
  }
11376
+ if (this.viaDotEnv) this.logger.debug(`Read ${this.viaDotEnv.keys.join(", ")} from ${this.viaDotEnv.file}.`);
10904
11377
  this.startTime = (/* @__PURE__ */ new Date()).toISOString();
10905
11378
  this.testPathBase = configDirFromConfig(config);
10906
11379
  this.playwrightVersion = config.version;
10907
11380
  this.maxFailures = config.maxFailures ?? 0;
10908
11381
  if (this.options.captureResources !== false) {
10909
- this.resourcesFile = path24.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
11382
+ this.resourcesFile = path25.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
10910
11383
  process.env[PIWI_RESOURCES_RESULTS_ENV] = this.resourcesFile;
10911
11384
  try {
10912
11385
  this.sampler = new RunSampler({ outputDirs: (config.projects ?? []).map((project) => project.outputDir) });
@@ -10975,8 +11448,9 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10975
11448
  );
10976
11449
  }
10977
11450
  if (coverage.server !== "off") this.lastTestWatch = new LastTestWatch(this.plannedTests.length);
10978
- if (coverage.browser !== "off" || coverage.server !== "off") {
11451
+ if (coverage.browser !== "off" || coverage.server !== "off" || coverage.css === "rules") {
10979
11452
  if (coverage.browser !== "off") process.env[PIWI_ENV_KEYS.coverageBrowser] = coverage.browser;
11453
+ if (coverage.css === "rules") process.env[PIWI_ENV_KEYS.coverageCss] = coverage.css;
10980
11454
  if (coverage.server !== "off") {
10981
11455
  process.env[PIWI_ENV_KEYS.coverageServer] = coverage.server;
10982
11456
  }
@@ -11001,6 +11475,22 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11001
11475
  // the dashboard shows the real total from the first render.
11002
11476
  this.plannedTests.length
11003
11477
  );
11478
+ if (this.streamManager && this.options.captureResources !== false) {
11479
+ const live = new LiveResources(
11480
+ this.streamManager,
11481
+ this.httpClient,
11482
+ {
11483
+ sampler: this.sampler,
11484
+ censuses: () => this.resourceCensuses,
11485
+ shardIndex: this.shardInfo?.current ?? null,
11486
+ runStartedAt: Date.now()
11487
+ },
11488
+ this.logger
11489
+ );
11490
+ this.streamManager.onWatched = () => void live.tick();
11491
+ live.start();
11492
+ this.liveResources = live;
11493
+ }
11004
11494
  }
11005
11495
  /**
11006
11496
  * Settle each planned test's external ID before any test runs, name the IDs two tests share, and record them on
@@ -11184,10 +11674,11 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11184
11674
  if (frames.length) testCase.testSourceFrames = frames;
11185
11675
  }
11186
11676
  if (this.options.collectPerformanceMetrics && result.steps?.length > 0) {
11187
- testCase.performanceMetrics = collectStepMetrics(result.steps, result.errors);
11677
+ const pauses = readPauses(result.attachments?.find((a) => a.name === ATTACHMENT_NAMES.pauses)?.body);
11678
+ testCase.performanceMetrics = collectStepMetrics(result.steps, result.errors, pauses);
11188
11679
  const stepEvents = extractTestStepEvents(result.steps, result.startTime);
11189
11680
  const waitEvents = extractWaitEvents(result.steps);
11190
- const allEvents = [...stepEvents, ...waitEvents];
11681
+ const allEvents = [...stepEvents, ...pauseStepEvents(pauses), ...waitEvents];
11191
11682
  if (allEvents.length > 0) testCase.stepEvents = allEvents;
11192
11683
  }
11193
11684
  if (this.options.collectPerformanceMetrics && result.attachments) {
@@ -11275,7 +11766,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11275
11766
  if (!file.path || INTERNAL_ATTACHMENT_NAMES.has(file.name)) continue;
11276
11767
  try {
11277
11768
  const kind = artifactKind(file.name, file.contentType);
11278
- const bytes = fs24.statSync(file.path).size;
11769
+ const bytes = fs25.statSync(file.path).size;
11279
11770
  artifacts[kind] += bytes;
11280
11771
  this.artifactBytes[kind] += bytes;
11281
11772
  } catch {
@@ -11302,14 +11793,16 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11302
11793
  */
11303
11794
  async reportResources() {
11304
11795
  if (this.options.captureResources === false) return;
11796
+ await this.liveResources?.stop();
11797
+ this.liveResources = null;
11305
11798
  const profile = this.sampler ? await this.sampler.stop() : null;
11306
11799
  const samples = this.sampler?.timedSeries() ?? null;
11307
11800
  this.sampler = null;
11308
11801
  const censuses = [...this.resourceCensuses];
11309
11802
  if (this.resourcesFile) {
11310
11803
  try {
11311
- censuses.push(...parseResourceResults(fs24.readFileSync(this.resourcesFile, "utf8")));
11312
- fs24.rmSync(this.resourcesFile, { force: true });
11804
+ censuses.push(...parseResourceResults(fs25.readFileSync(this.resourcesFile, "utf8")));
11805
+ fs25.rmSync(this.resourcesFile, { force: true });
11313
11806
  } catch {
11314
11807
  }
11315
11808
  }
@@ -11534,7 +12027,7 @@ async function resolveSelection(options = {}) {
11534
12027
  }
11535
12028
 
11536
12029
  // src/internal/capture/capture-fixtures.ts
11537
- var path28 = __toESM(require("path"));
12030
+ var path30 = __toESM(require("path"));
11538
12031
  var import_node_zlib2 = require("zlib");
11539
12032
 
11540
12033
  // ../picker-dom/src/probe.ts
@@ -11993,8 +12486,8 @@ function installPickerOverlay(arg) {
11993
12486
  return root && root !== doc && root.host ? root.host : null;
11994
12487
  };
11995
12488
  const targetOf = (e) => {
11996
- const path32 = typeof e.composedPath === "function" ? e.composedPath() : null;
11997
- const first = path32 && path32.length ? path32[0] : null;
12489
+ const path34 = typeof e.composedPath === "function" ? e.composedPath() : null;
12490
+ const first = path34 && path34.length ? path34[0] : null;
11998
12491
  return first && first.nodeType === 1 ? first : e.target;
11999
12492
  };
12000
12493
  const buildChain = (raw) => {
@@ -12698,8 +13191,11 @@ function showPauseBar(arg) {
12698
13191
  };
12699
13192
  const buttons = [
12700
13193
  ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
12701
- ["Step", "step", "Pause again at the next action", false],
13194
+ ["Step", "step", "Pause again before the next call", false],
12702
13195
  ["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
13196
+ ...arg.record ? [
13197
+ ["Record here", "record", "Record steps in this page: the editor writes them before this line", false]
13198
+ ] : [],
12703
13199
  ["Finish", "finish", "Run on and pause no more in this test", false]
12704
13200
  ];
12705
13201
  for (const [label, choice, hint, primary] of buttons) {
@@ -12865,7 +13361,7 @@ var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "dat
12865
13361
  function generateAlternatives(attrs, options = {}) {
12866
13362
  const alts = [];
12867
13363
  const seen = /* @__PURE__ */ new Set();
12868
- const add = (loc) => {
13364
+ const add2 = (loc) => {
12869
13365
  if (!seen.has(loc.locator)) {
12870
13366
  seen.add(loc.locator);
12871
13367
  alts.push(loc);
@@ -12882,7 +13378,7 @@ function generateAlternatives(attrs, options = {}) {
12882
13378
  const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
12883
13379
  const testId = attr(attrs, testIdAttribute);
12884
13380
  if (testId && isUnique(counts?.testId)) {
12885
- add({
13381
+ add2({
12886
13382
  locator: `getByTestId(${quoteJsString(testId)})`,
12887
13383
  method: "getByTestId",
12888
13384
  args: { testId },
@@ -12893,7 +13389,7 @@ function generateAlternatives(attrs, options = {}) {
12893
13389
  const levelPart = level != null ? `, level: ${level}` : "";
12894
13390
  const withLevel = (base) => level != null ? { ...base, level } : base;
12895
13391
  if (role && accessibleName) {
12896
- add({
13392
+ add2({
12897
13393
  locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(accessibleName)}${levelPart} })`,
12898
13394
  method: "getByRole",
12899
13395
  args: withLevel({ role, name: accessibleName }),
@@ -12902,7 +13398,7 @@ function generateAlternatives(attrs, options = {}) {
12902
13398
  }
12903
13399
  const ariaLabel = attr(attrs, "aria-label");
12904
13400
  if (role && ariaLabel && ariaLabel !== accessibleName) {
12905
- add({
13401
+ add2({
12906
13402
  locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(ariaLabel)}${levelPart} })`,
12907
13403
  method: "getByRole",
12908
13404
  args: withLevel({ role, name: ariaLabel }),
@@ -12913,7 +13409,7 @@ function generateAlternatives(attrs, options = {}) {
12913
13409
  const label = attr(attrs, "aria-label");
12914
13410
  const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
12915
13411
  if (labelBacked) {
12916
- add({
13412
+ add2({
12917
13413
  locator: `getByLabel(${quoteJsString(accessibleName)})`,
12918
13414
  method: "getByLabel",
12919
13415
  args: { label: accessibleName },
@@ -12923,7 +13419,7 @@ function generateAlternatives(attrs, options = {}) {
12923
13419
  }
12924
13420
  const placeholder = attr(attrs, "placeholder");
12925
13421
  if (placeholder) {
12926
- add({
13422
+ add2({
12927
13423
  locator: `getByPlaceholder(${quoteJsString(placeholder)})`,
12928
13424
  method: "getByPlaceholder",
12929
13425
  args: { placeholder },
@@ -12931,7 +13427,7 @@ function generateAlternatives(attrs, options = {}) {
12931
13427
  });
12932
13428
  }
12933
13429
  if (text && text.length < 80) {
12934
- add({
13430
+ add2({
12935
13431
  locator: `getByText(${quoteJsString(text)})`,
12936
13432
  method: "getByText",
12937
13433
  args: { text },
@@ -12941,7 +13437,7 @@ function generateAlternatives(attrs, options = {}) {
12941
13437
  const id = attr(attrs, "id");
12942
13438
  if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
12943
13439
  const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
12944
- add({
13440
+ add2({
12945
13441
  locator: `locator(${quoteJsString(selector)})`,
12946
13442
  method: "locator",
12947
13443
  args: { selector },
@@ -12951,7 +13447,7 @@ function generateAlternatives(attrs, options = {}) {
12951
13447
  const name = attr(attrs, "name");
12952
13448
  if (name && selectable("name") && isUnique(counts?.name)) {
12953
13449
  const selector = `[name="${escCssAttrValue(name)}"]`;
12954
- add({
13450
+ add2({
12955
13451
  locator: `locator(${quoteJsString(selector)})`,
12956
13452
  method: "locator",
12957
13453
  args: { selector },
@@ -12960,7 +13456,7 @@ function generateAlternatives(attrs, options = {}) {
12960
13456
  }
12961
13457
  const alt = attr(attrs, "alt");
12962
13458
  if (alt) {
12963
- add({
13459
+ add2({
12964
13460
  locator: `getByAltText(${quoteJsString(alt)})`,
12965
13461
  method: "getByAltText",
12966
13462
  args: { text: alt },
@@ -12969,7 +13465,7 @@ function generateAlternatives(attrs, options = {}) {
12969
13465
  }
12970
13466
  const title = attr(attrs, "title");
12971
13467
  if (title) {
12972
- add({
13468
+ add2({
12973
13469
  locator: `getByTitle(${quoteJsString(title)})`,
12974
13470
  method: "getByTitle",
12975
13471
  args: { title },
@@ -12987,7 +13483,7 @@ function generateAlternatives(attrs, options = {}) {
12987
13483
  if (scopedCount(anc) !== 1) continue;
12988
13484
  if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
12989
13485
  testIdAnchorDone = true;
12990
- add({
13486
+ add2({
12991
13487
  locator: `getByTestId(${quoteJsString(anc.testId)}).${leaf.expr}`,
12992
13488
  method: leaf.method,
12993
13489
  args: { ...leaf.args, anchorTestId: anc.testId },
@@ -12997,7 +13493,7 @@ function generateAlternatives(attrs, options = {}) {
12997
13493
  if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
12998
13494
  idAnchorDone = true;
12999
13495
  const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
13000
- add({
13496
+ add2({
13001
13497
  locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
13002
13498
  method: leaf.method,
13003
13499
  args: { ...leaf.args, anchorSelector },
@@ -13008,7 +13504,7 @@ function generateAlternatives(attrs, options = {}) {
13008
13504
  dataAnchorDone = true;
13009
13505
  const { name: name2, value } = anc.dataAttr;
13010
13506
  const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
13011
- add({
13507
+ add2({
13012
13508
  locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
13013
13509
  method: leaf.method,
13014
13510
  args: { ...leaf.args, anchorSelector },
@@ -13018,7 +13514,7 @@ function generateAlternatives(attrs, options = {}) {
13018
13514
  const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
13019
13515
  if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
13020
13516
  roleAnchorDone = true;
13021
- add({
13517
+ add2({
13022
13518
  locator: `getByRole(${quoteJsString(ancestorRole)}).${leaf.expr}`,
13023
13519
  method: leaf.method,
13024
13520
  args: { ...leaf.args, anchorRole: ancestorRole },
@@ -13027,7 +13523,7 @@ function generateAlternatives(attrs, options = {}) {
13027
13523
  }
13028
13524
  if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
13029
13525
  filterAnchorDone = true;
13030
- add({
13526
+ add2({
13031
13527
  locator: `getByRole(${quoteJsString(ancestorRole)}).filter({ hasText: ${quoteJsString(anc.filterText)} }).${leaf.expr}`,
13032
13528
  method: leaf.method,
13033
13529
  args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
@@ -13046,7 +13542,7 @@ function generateAlternatives(attrs, options = {}) {
13046
13542
  );
13047
13543
  const pos = attrs.rolePosition;
13048
13544
  if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
13049
- add({
13545
+ add2({
13050
13546
  locator: `getByRole(${rolePart})`,
13051
13547
  method: "getByRole",
13052
13548
  args: leafArgs,
@@ -13065,7 +13561,7 @@ function generateAlternatives(attrs, options = {}) {
13065
13561
  const countsClasses = counts?.classes;
13066
13562
  const classes = clsStr.split(/\s+/).filter((c) => c.length > 1 && /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(c) && isUnique(countsClasses?.[c])).map((cls) => ({ cls, score: classifyCssStability(cls) })).sort((a, b) => b.score - a.score).slice(0, 3);
13067
13563
  for (const { cls, score } of classes) {
13068
- add({
13564
+ add2({
13069
13565
  locator: `locator(${quoteJsString(`.${cls}`)})`,
13070
13566
  method: "locator",
13071
13567
  args: { selector: `.${cls}` },
@@ -13122,7 +13618,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
13122
13618
  const seen = /* @__PURE__ */ new Set();
13123
13619
  const rolePart = leaf.level != null ? `${quoteJsString(leaf.role)}, { level: ${leaf.level} }` : quoteJsString(leaf.role);
13124
13620
  const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
13125
- const add = (l) => {
13621
+ const add2 = (l) => {
13126
13622
  if (!seen.has(l.locator)) {
13127
13623
  seen.add(l.locator);
13128
13624
  out.push(l);
@@ -13132,7 +13628,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
13132
13628
  if (anchor.scopedLeafCount !== 1) continue;
13133
13629
  const seg = anchorSegment(anchor);
13134
13630
  if (!seg) continue;
13135
- add({
13631
+ add2({
13136
13632
  locator: `${seg.code}.getByRole(${rolePart})`,
13137
13633
  method: "getByRole",
13138
13634
  args: { ...leafArgs, ...seg.flatArgs },
@@ -13145,7 +13641,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
13145
13641
  if (segments.every((s) => s !== null)) {
13146
13642
  const innermost = segments[segments.length - 1];
13147
13643
  const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
13148
- add({
13644
+ add2({
13149
13645
  locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
13150
13646
  method: "getByRole",
13151
13647
  // Flat args describe the innermost anchor (what the healing fingerprint
@@ -13175,21 +13671,21 @@ var MAX_INDETERMINATE = 20;
13175
13671
  var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
13176
13672
  var LEVEL = `${STEP}(?:/${STEP})*`;
13177
13673
  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);
13674
+ function isElementPath(path34) {
13675
+ return typeof path34 === "string" && path34.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path34);
13180
13676
  }
13181
13677
  var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
13182
13678
  function elementRef(v) {
13183
13679
  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) : "" };
13680
+ const { path: path34, label } = v;
13681
+ if (!isElementPath(path34)) return null;
13682
+ return { path: path34, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
13187
13683
  }
13188
13684
  function textPoint(v) {
13189
13685
  if (!v || typeof v !== "object") return null;
13190
- const { path: path32, offset } = v;
13686
+ const { path: path34, offset } = v;
13191
13687
  const at = offsetOf(offset);
13192
- return isElementPath(path32) && at !== null ? { path: path32, offset: at } : null;
13688
+ return isElementPath(path34) && at !== null ? { path: path34, offset: at } : null;
13193
13689
  }
13194
13690
  function sanitizePageInteraction(raw) {
13195
13691
  if (!raw || typeof raw !== "object") return {};
@@ -13220,7 +13716,7 @@ function sanitizePageInteraction(raw) {
13220
13716
  }
13221
13717
 
13222
13718
  // src/internal/capture/locator-healing.ts
13223
- var path25 = __toESM(require("path"));
13719
+ var path26 = __toESM(require("path"));
13224
13720
 
13225
13721
  // ../core/src/locator-fingerprint.ts
13226
13722
  var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
@@ -13605,10 +14101,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
13605
14101
  }
13606
14102
  let rel = file;
13607
14103
  try {
13608
- rel = path25.relative(process.cwd(), file);
14104
+ rel = path26.relative(process.cwd(), file);
13609
14105
  } catch {
13610
14106
  }
13611
- rel = rel.split(path25.sep).join("/");
14107
+ rel = rel.split(path26.sep).join("/");
13612
14108
  if (rel.startsWith("./")) rel = rel.slice(2);
13613
14109
  return `${rel}:${m[3]}:${m[4]}`;
13614
14110
  }
@@ -13838,7 +14334,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
13838
14334
  }
13839
14335
 
13840
14336
  // src/internal/capture/pick-on-failure.ts
13841
- var path26 = __toESM(require("path"));
14337
+ var path27 = __toESM(require("path"));
13842
14338
  function parseLeafLocatorExpression(rawExpr) {
13843
14339
  const leaf = parseLeafLocatorCall(rawExpr);
13844
14340
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -13853,37 +14349,60 @@ ${err.stack ?? ""}`));
13853
14349
  const parsed2 = parseLeafLocatorExpression(header);
13854
14350
  if (!parsed2) continue;
13855
14351
  const loc = err.location;
13856
- const location = loc ? `${path26.relative(process.cwd(), loc.file).split(path26.sep).join("/")}:${loc.line}:${loc.column}` : null;
14352
+ const location = loc ? `${path27.relative(process.cwd(), loc.file).split(path27.sep).join("/")}:${loc.line}:${loc.column}` : null;
13857
14353
  return { method: parsed2.method, args: parsed2.args, location };
13858
14354
  }
13859
14355
  return null;
13860
14356
  }
13861
- async function cleanupPicker(page) {
14357
+ var PICK_ENDED = "__piwiPickEnded";
14358
+ async function cleanupPicker(page, ended = false) {
13862
14359
  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
- });
14360
+ await page.evaluate(
14361
+ ([endedKey, unanswered]) => {
14362
+ const g = globalThis;
14363
+ if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
14364
+ if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
14365
+ delete g.__piwiPickCleanup;
14366
+ delete g.__piwiAnchorCleanup;
14367
+ delete g.__piwiPickState;
14368
+ delete g.__piwiPickChoice;
14369
+ delete g.__piwiPickedElement;
14370
+ delete g.__piwiAnchorState;
14371
+ delete g.__piwiPickAnchors;
14372
+ delete g.__piwiPickChainCount;
14373
+ if (unanswered) g[endedKey] = true;
14374
+ else delete g[endedKey];
14375
+ },
14376
+ [PICK_ENDED, ended]
14377
+ );
13876
14378
  } catch {
13877
14379
  }
13878
14380
  }
13879
- async function pickLocator(page, probe, failing) {
14381
+ function unlessEnded(run, ended) {
14382
+ if (!ended) return run;
14383
+ return Promise.race([
14384
+ run,
14385
+ new Promise((resolve11) => {
14386
+ if (ended.aborted) resolve11(void 0);
14387
+ else ended.addEventListener("abort", () => resolve11(void 0), { once: true });
14388
+ })
14389
+ ]);
14390
+ }
14391
+ async function pickLocator(page, probe, failing, ended) {
14392
+ const answered = async (name) => {
14393
+ const wait = page.waitForFunction(
14394
+ ([key, endedKey]) => globalThis[key] !== void 0 || globalThis[endedKey] === true,
14395
+ [name, PICK_ENDED],
14396
+ { timeout: 0, polling: 250 }
14397
+ );
14398
+ await unlessEnded(wait, ended);
14399
+ return !ended?.aborted;
14400
+ };
13880
14401
  try {
13881
14402
  const overlayArg = { transport: "global", failing };
14403
+ await page.evaluate((endedKey) => delete globalThis[endedKey], PICK_ENDED);
13882
14404
  await page.evaluate(installPickerOverlay, overlayArg);
13883
- await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
13884
- timeout: 0,
13885
- polling: 250
13886
- });
14405
+ if (!await answered("__piwiPickState")) return null;
13887
14406
  const state = await page.evaluate(() => globalThis.__piwiPickState);
13888
14407
  if (state !== "picked") return null;
13889
14408
  const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
@@ -13904,10 +14423,7 @@ async function pickLocator(page, probe, failing) {
13904
14423
  leafLevel: level,
13905
14424
  leafTestId: attrs.attributes["data-testid"] ?? null
13906
14425
  });
13907
- await page.waitForFunction(() => globalThis.__piwiAnchorState !== void 0, void 0, {
13908
- timeout: 0,
13909
- polling: 250
13910
- });
14426
+ if (!await answered("__piwiAnchorState")) return null;
13911
14427
  const anchorResult = await page.evaluate(() => {
13912
14428
  const g = globalThis;
13913
14429
  return { state: g.__piwiAnchorState, anchors: g.__piwiPickAnchors ?? [], chainCount: g.__piwiPickChainCount };
@@ -13929,10 +14445,7 @@ async function pickLocator(page, probe, failing) {
13929
14445
  failing,
13930
14446
  choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
13931
14447
  });
13932
- await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
13933
- timeout: 0,
13934
- polling: 250
13935
- });
14448
+ if (!await answered("__piwiPickChoice")) return null;
13936
14449
  const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
13937
14450
  if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
13938
14451
  const picked = { ...ranked[choice], pickedByUser: true };
@@ -13951,7 +14464,7 @@ async function pickLocator(page, probe, failing) {
13951
14464
  ...anchors.length > 0 ? { anchors } : {}
13952
14465
  };
13953
14466
  } finally {
13954
- await cleanupPicker(page);
14467
+ await cleanupPicker(page, ended?.aborted);
13955
14468
  }
13956
14469
  }
13957
14470
  async function runLocatorPicker(page, testInfo, failed, probe) {
@@ -14006,7 +14519,7 @@ function applyPickToSnapshots(snapshots, pick) {
14006
14519
  }
14007
14520
 
14008
14521
  // src/internal/capture/pause-at.ts
14009
- var path27 = __toESM(require("path"));
14522
+ var path28 = __toESM(require("path"));
14010
14523
 
14011
14524
  // ../core/src/editor-send.ts
14012
14525
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
@@ -14047,18 +14560,32 @@ var PauseSet = class {
14047
14560
  return col > 0 && this.lines.has(callerLocation.slice(0, col));
14048
14561
  }
14049
14562
  };
14050
- var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
14563
+ var NO_BREAKPOINTS = new PauseSet(/* @__PURE__ */ new Set());
14051
14564
  function callSiteFile(file, cwd) {
14052
14565
  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, "/");
14566
+ const absolute = path28.isAbsolute(file) || path28.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
14567
+ let rel = absolute ? path28.relative(cwd, file) : path28.normalize(slashed);
14568
+ rel = rel.split(path28.sep).join("/").replace(/\\/g, "/");
14056
14569
  if (rel.startsWith("./")) rel = rel.slice(2);
14057
14570
  return rel;
14058
14571
  }
14572
+ function stopCallSite(location, cwd = process.cwd()) {
14573
+ if (!location.line) return null;
14574
+ return `${callSiteFile(location.file, cwd)}:${location.line}:${location.column ?? 0}`;
14575
+ }
14576
+ function callSiteLine(callSite) {
14577
+ const col = callSite.lastIndexOf(":");
14578
+ return col > 0 ? callSite.slice(0, col) : callSite;
14579
+ }
14580
+ function inFlightCallSite(inFlight) {
14581
+ const [first] = inFlight;
14582
+ if (first === void 0) return null;
14583
+ const line = callSiteLine(first);
14584
+ return inFlight.every((site) => callSiteLine(site) === line) ? first : null;
14585
+ }
14059
14586
  var ignoredLogged = /* @__PURE__ */ new Set();
14060
14587
  function parsePauseAt(value, cwd = process.cwd()) {
14061
- if (!value?.trim()) return EMPTY;
14588
+ if (!value?.trim()) return NO_BREAKPOINTS;
14062
14589
  const lines = /* @__PURE__ */ new Set();
14063
14590
  for (const raw of value.split(";")) {
14064
14591
  const entry = raw.trim();
@@ -14074,7 +14601,7 @@ function parsePauseAt(value, cwd = process.cwd()) {
14074
14601
  }
14075
14602
  lines.add(`${callSiteFile(m[1], cwd)}:${line}`);
14076
14603
  }
14077
- return lines.size ? new PauseSet(lines) : EMPTY;
14604
+ return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
14078
14605
  }
14079
14606
  var parsed = null;
14080
14607
  function currentPauseSet() {
@@ -14084,27 +14611,121 @@ function currentPauseSet() {
14084
14611
  }
14085
14612
  function pauseGateFromTestInfo(testInfo) {
14086
14613
  const use = testInfo.project?.use ?? {};
14087
- return { ci: process.env.CI, headless: use.headless };
14614
+ const debug = testInfo._configInternal?.configCLIOverrides?.debug;
14615
+ return { ci: process.env.CI, headless: use.headless, pwdebug: process.env.PWDEBUG, debug };
14088
14616
  }
14089
14617
  function pauseSkipReason(gate) {
14090
14618
  if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
14091
14619
  if (gate.headless !== false)
14092
14620
  return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
14621
+ if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug))
14622
+ return "Playwright's Inspector runs the test (PWDEBUG) \u2014 run without --debug";
14623
+ if (typeof gate.debug === "string" && gate.debug)
14624
+ return `Playwright's debugger runs the test (--debug=${gate.debug}) \u2014 run without --debug`;
14093
14625
  return null;
14094
14626
  }
14095
- function rearmedTimeout(before, pausedMs) {
14096
- return before > 0 ? before + Math.max(0, Math.round(pausedMs)) : 0;
14627
+ function atStop(state, callSite) {
14628
+ if (callSite && !state.finished) {
14629
+ if (state.step) {
14630
+ state.step = false;
14631
+ return { show: "step" };
14632
+ }
14633
+ if (state.set.matches(callSite)) return { show: "breakpoint" };
14634
+ }
14635
+ return { go: goOn(state) };
14636
+ }
14637
+ function goOn(state, answer) {
14638
+ if (answer === "step") state.step = true;
14639
+ else if (answer === "finish") state.finished = true;
14640
+ return !state.finished && (state.step || state.set.size > 0) ? "next" : "resume";
14641
+ }
14642
+ function contextDebugger(context) {
14643
+ const dbg = context.debugger;
14644
+ return typeof dbg?.requestPause === "function" && typeof dbg.on === "function" ? dbg : null;
14645
+ }
14646
+ var WATCHED = /* @__PURE__ */ new WeakSet();
14647
+ function watchStops(context, onStop) {
14648
+ const dbg = contextDebugger(context);
14649
+ if (!dbg) return false;
14650
+ if (WATCHED.has(context)) return true;
14651
+ WATCHED.add(context);
14652
+ inspectorSwitch.watching = true;
14653
+ syncInspectorSwitch();
14654
+ dbg.on("pausedstatechanged", () => onStop(context));
14655
+ return true;
14656
+ }
14657
+ var NO_INSPECTOR_ENV = "PW_CODEGEN_NO_INSPECTOR";
14658
+ var inspectorSwitch = {
14659
+ /** Set by Piwi, so Piwi's to clear. */
14660
+ set: false,
14661
+ /** Piwi answers the stops of a context in this worker. */
14662
+ watching: false,
14663
+ /** Every `page.pause()` of the worker's Playwright reaches it through Piwi (`markPausesWrapped`). */
14664
+ wrapped: false,
14665
+ /** The `page.pause()` calls left to Playwright that are running (`withInspector`). */
14666
+ open: 0
14667
+ };
14668
+ function syncInspectorSwitch() {
14669
+ const closed = inspectorSwitch.watching && inspectorSwitch.wrapped && inspectorSwitch.open === 0;
14670
+ if (closed && process.env[NO_INSPECTOR_ENV] === void 0) {
14671
+ process.env[NO_INSPECTOR_ENV] = "1";
14672
+ inspectorSwitch.set = true;
14673
+ } else if (!closed && inspectorSwitch.set) {
14674
+ if (process.env[NO_INSPECTOR_ENV] === "1") delete process.env[NO_INSPECTOR_ENV];
14675
+ inspectorSwitch.set = false;
14676
+ }
14677
+ }
14678
+ function markPausesWrapped() {
14679
+ inspectorSwitch.wrapped = true;
14680
+ syncInspectorSwitch();
14681
+ }
14682
+ async function withInspector(run) {
14683
+ inspectorSwitch.open++;
14684
+ syncInspectorSwitch();
14685
+ try {
14686
+ return await run();
14687
+ } finally {
14688
+ inspectorSwitch.open--;
14689
+ syncInspectorSwitch();
14690
+ }
14691
+ }
14692
+ async function steerDebugger(context, how) {
14693
+ const dbg = contextDebugger(context);
14694
+ if (!dbg) return;
14695
+ try {
14696
+ await internalCall(
14697
+ context,
14698
+ () => how === "pause" ? dbg.requestPause() : how === "next" ? dbg.next() : dbg.resume()
14699
+ );
14700
+ } catch {
14701
+ }
14702
+ }
14703
+ async function withTestTimeoutHeld(testInfo, run) {
14704
+ const set = testInfo._setIgnoreTimeouts;
14705
+ if (typeof set !== "function") return run();
14706
+ set.call(testInfo, true);
14707
+ try {
14708
+ return await run();
14709
+ } finally {
14710
+ set.call(testInfo, false);
14711
+ }
14712
+ }
14713
+ function testTimeSlot(testInfo) {
14714
+ try {
14715
+ const slot = testInfo._timeoutManager?.defaultSlot?.();
14716
+ return slot && typeof slot.elapsed === "number" ? slot : null;
14717
+ } catch {
14718
+ return null;
14719
+ }
14720
+ }
14721
+ function restoreTestTime(testInfo, heldAt) {
14722
+ const slot = testTimeSlot(testInfo);
14723
+ if (slot && Number.isFinite(heldAt) && heldAt >= 0 && heldAt < slot.elapsed) slot.elapsed = heldAt;
14097
14724
  }
14098
14725
  function pausePlace(callerLocation) {
14099
- const col = callerLocation.lastIndexOf(":");
14100
- const fileLine = col > 0 ? callerLocation.slice(0, col) : callerLocation;
14726
+ const fileLine = callSiteLine(callerLocation);
14101
14727
  return fileLine.slice(fileLine.lastIndexOf("/") + 1);
14102
14728
  }
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
14729
  function pickPlace(callerLocation) {
14109
14730
  const m = /^(.+):(\d+):\d+$/.exec(callerLocation);
14110
14731
  if (!m) return null;
@@ -14149,57 +14770,571 @@ function logPauseSkipOnce(reason) {
14149
14770
  skipLogged = true;
14150
14771
  console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt} is set but breakpoints are skipped: ${reason}`);
14151
14772
  }
14773
+ var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
14774
+ var MAX_NAVIGATED = 10;
14152
14775
  var HIGHLIGHT_MS = 2e3;
14153
- async function pauseHere(page, testInfo, point, probe, onPick) {
14154
- const before = testInfo.timeout;
14155
- const started = Date.now();
14776
+ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
14156
14777
  let shown = null;
14157
14778
  try {
14158
- testInfo.setTimeout(0);
14159
14779
  const highlight = point.target?.highlight;
14160
14780
  if (typeof highlight === "function") {
14161
14781
  let timer;
14162
14782
  shown = await Promise.race([
14163
14783
  Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
14164
- new Promise((resolve9) => {
14165
- timer = setTimeout(() => resolve9(null), HIGHLIGHT_MS);
14784
+ new Promise((resolve11) => {
14785
+ timer = setTimeout(() => resolve11(null), HIGHLIGHT_MS);
14166
14786
  })
14167
14787
  ]);
14168
14788
  clearTimeout(timer);
14169
14789
  }
14170
14790
  const arg = {
14171
14791
  place: pausePlace(point.location),
14172
- attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
14173
- action: point.action,
14174
- locator: point.locator
14792
+ attempt: point.attempt,
14793
+ action: point.call,
14794
+ locator: null,
14795
+ record: !!onRecord
14175
14796
  };
14176
14797
  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).`
14798
+ `[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
14799
  );
14800
+ let navigated = 0;
14179
14801
  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);
14802
+ try {
14803
+ if (ended?.aborted) return "resume";
14804
+ await page.evaluate(showPauseBar, arg);
14805
+ await unlessEnded(page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 }), ended);
14806
+ if (ended?.aborted) return "resume";
14807
+ const choice = await page.evaluate(takePauseState);
14808
+ navigated = 0;
14809
+ if (choice === "resume" || choice === "step" || choice === "finish") return choice;
14810
+ if (choice === "record" && onRecord) {
14811
+ await unlessEnded(
14812
+ onRecord(ended).catch(() => void 0),
14813
+ ended
14814
+ );
14815
+ continue;
14816
+ }
14817
+ if (choice !== "pick") continue;
14818
+ const picked = await pickLocator(page, probe, null, ended).catch(() => null);
14819
+ if (picked && !ended?.aborted) await onPick(picked);
14820
+ } catch (error) {
14821
+ if (page.isClosed() || !NAVIGATED.test(String(error)) || ++navigated > MAX_NAVIGATED) throw error;
14822
+ await page.waitForLoadState("domcontentloaded").catch(() => void 0);
14823
+ }
14187
14824
  }
14188
14825
  } catch {
14189
14826
  return "resume";
14190
14827
  } finally {
14191
14828
  try {
14829
+ if (ended?.aborted) await page.evaluate(takePauseState);
14192
14830
  const dispose = shown?.dispose;
14193
14831
  const hide = page.hideHighlight;
14194
14832
  if (typeof dispose === "function") await dispose.call(shown);
14195
14833
  else if (typeof hide === "function") await hide.call(page);
14196
14834
  } catch {
14197
14835
  }
14836
+ }
14837
+ }
14838
+
14839
+ // src/internal/capture/record-handoff.ts
14840
+ var fs26 = __toESM(require("fs"));
14841
+ var net2 = __toESM(require("net"));
14842
+ var path29 = __toESM(require("path"));
14843
+
14844
+ // ../core/src/ide-recorder.ts
14845
+ var IDE_RECORDER_BINDING = "__piwiRecorder";
14846
+ var IDE_DISPATCH_GLOBAL = "__piwiIdeDispatch";
14847
+ var RECORD_CHANNEL_ENV = "PIWI_RECORD_CHANNEL";
14848
+ var MAX_HANDOFF_LINE = 64e6;
14849
+ function parseRecordChannel(value) {
14850
+ const text = value?.trim() ?? "";
14851
+ const hash = text.lastIndexOf("#");
14852
+ if (hash <= 0) return null;
14853
+ const token = text.slice(hash + 1);
14854
+ const path34 = text.slice(0, hash);
14855
+ return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path34.includes("\0") ? { path: path34, token } : null;
14856
+ }
14857
+ function isObject2(value) {
14858
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14859
+ }
14860
+ function parseHandoffToTest(line) {
14861
+ let value;
14862
+ try {
14863
+ value = JSON.parse(line);
14864
+ } catch {
14865
+ return null;
14866
+ }
14867
+ if (!isObject2(value)) return null;
14868
+ switch (value.type) {
14869
+ case "refused":
14870
+ return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
14871
+ case "load":
14872
+ return typeof value.bundle === "string" && value.bundle ? { type: "load", bundle: value.bundle } : null;
14873
+ case "answer":
14874
+ return typeof value.call === "number" ? { type: "answer", call: value.call, value: value.value } : null;
14875
+ case "dispatch":
14876
+ return { type: "dispatch", message: value.message };
14877
+ case "stop":
14878
+ return { type: "stop" };
14879
+ default:
14880
+ return null;
14881
+ }
14882
+ }
14883
+ function lineReader(onLine, onOverflow) {
14884
+ let buffered = "";
14885
+ return (chunk) => {
14886
+ buffered += chunk;
14887
+ for (let end = buffered.indexOf("\n"); end >= 0; end = buffered.indexOf("\n")) {
14888
+ const line = buffered.slice(0, end);
14889
+ buffered = buffered.slice(end + 1);
14890
+ if (line.trim()) onLine(line);
14891
+ }
14892
+ if (buffered.length > MAX_HANDOFF_LINE) {
14893
+ buffered = "";
14894
+ onOverflow();
14895
+ }
14896
+ };
14897
+ }
14898
+
14899
+ // src/internal/capture/record-handoff.ts
14900
+ var START_WAIT_MS = 6e4;
14901
+ var NEW_TAB_NOTICE = "A new tab opened: what you do there is not recorded yet. Record in the first tab.";
14902
+ var relays = /* @__PURE__ */ new WeakMap();
14903
+ function idleAnswer(payload) {
14904
+ const request3 = payload ?? {};
14905
+ if (request3.kind === "local-get") return {};
14906
+ if (request3.kind !== "message") return null;
14907
+ if (request3.message?.type === "piwi-ping") return { ok: true };
14908
+ if (request3.message?.type === "piwi-session-storage") {
14909
+ return request3.message.op === "get" ? { ok: true, items: {} } : { ok: true };
14910
+ }
14911
+ return { ok: false, error: "No recording runs in this page." };
14912
+ }
14913
+ function canRecordFromHere(channel = process.env[RECORD_CHANNEL_ENV]) {
14914
+ return parseRecordChannel(channel) !== null;
14915
+ }
14916
+ function handoffPlace(location, cwd = process.cwd()) {
14917
+ const m = /^(.+):(\d+):\d+$/.exec(location);
14918
+ if (!m) return null;
14919
+ return { file: path29.resolve(cwd, m[1]), line: Number(m[2]) };
14920
+ }
14921
+ async function recordFromHere(options) {
14922
+ const channel = parseRecordChannel(options.channel ?? process.env[RECORD_CHANNEL_ENV]);
14923
+ const place = handoffPlace(options.location);
14924
+ if (!channel || !place) return "this run names no editor to record into.";
14925
+ const { page, testInfo, ended } = options;
14926
+ if (ended?.aborted) return "the test ended.";
14927
+ const use = testInfo.project?.use ?? {};
14928
+ const context = page.context();
14929
+ return new Promise((resolve11) => {
14930
+ const socket = net2.connect(channel.path);
14931
+ socket.setEncoding("utf8");
14932
+ const answers = /* @__PURE__ */ new Map();
14933
+ let calls = 0;
14934
+ let attached = false;
14935
+ let recording = false;
14936
+ let settled = false;
14937
+ let initScript;
14938
+ const send = (message) => {
14939
+ if (!socket.destroyed) socket.write(`${JSON.stringify(message)}
14940
+ `);
14941
+ };
14942
+ const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
14943
+ const onGone = () => {
14944
+ if (attached) send({ type: "closed" });
14945
+ finish("the page closed.");
14946
+ };
14947
+ const onEnded = () => {
14948
+ try {
14949
+ if (recording) {
14950
+ void internalCall(
14951
+ page,
14952
+ () => page.evaluate(
14953
+ ([name, value]) => {
14954
+ const dispatch = globalThis[name];
14955
+ if (typeof dispatch === "function") dispatch(value);
14956
+ },
14957
+ [IDE_DISPATCH_GLOBAL, { type: "piwi-recording-stopped" }]
14958
+ )
14959
+ ).catch(() => void 0);
14960
+ }
14961
+ if (attached) send({ type: "closed" });
14962
+ finish("the test ended.");
14963
+ } catch {
14964
+ }
14965
+ };
14966
+ page.once("close", onGone);
14967
+ page.once("crash", onGone);
14968
+ ended?.addEventListener("abort", onEnded, { once: true });
14969
+ const startWait = setTimeout(() => finish("the editor did not start the recording."), START_WAIT_MS);
14970
+ const release = () => {
14971
+ const relay = relays.get(context);
14972
+ if (relay && relay.recording === relayed) relay.recording = null;
14973
+ void Promise.resolve(initScript?.dispose?.()).catch(() => void 0);
14974
+ initScript = void 0;
14975
+ };
14976
+ const finish = (said) => {
14977
+ if (settled) return;
14978
+ settled = true;
14979
+ clearTimeout(startWait);
14980
+ release();
14981
+ for (const [call, answer] of answers) {
14982
+ answers.delete(call);
14983
+ answer(null);
14984
+ }
14985
+ context.off("page", onPage);
14986
+ page.off("close", onGone);
14987
+ page.off("crash", onGone);
14988
+ ended?.removeEventListener("abort", onEnded);
14989
+ socket.end();
14990
+ resolve11(recording ? null : said);
14991
+ };
14992
+ const relayed = (source, payload) => new Promise((answer) => {
14993
+ const call = ++calls;
14994
+ answers.set(call, answer);
14995
+ const outside = source.page !== page || source.frame !== page.mainFrame();
14996
+ send({ type: "binding", call, outside, payload });
14997
+ });
14998
+ const load = async (bundle) => {
14999
+ if (!/^https?:/i.test(page.url())) {
15000
+ const why = `the recorder records http(s) pages only, and this page is ${page.url() || "blank"}.`;
15001
+ send({ type: "notice", message: `Nothing can be recorded here: ${why}` });
15002
+ finish(why);
15003
+ return;
15004
+ }
15005
+ let relay = relays.get(context);
15006
+ if (!relay) {
15007
+ const created = { recording: null, lingering: false };
15008
+ await context.exposeBinding(
15009
+ IDE_RECORDER_BINDING,
15010
+ (source, payload) => created.recording ? created.recording(source, payload) : idleAnswer(payload)
15011
+ );
15012
+ relays.set(context, created);
15013
+ relay = created;
15014
+ }
15015
+ if (settled) return;
15016
+ relay.recording = relayed;
15017
+ if (!relay.lingering) {
15018
+ initScript = await context.addInitScript({ path: bundle });
15019
+ if (typeof initScript?.dispose !== "function") relay.lingering = true;
15020
+ }
15021
+ if (settled) return release();
15022
+ await page.evaluate(fs26.readFileSync(bundle, "utf-8"));
15023
+ if (settled) return release();
15024
+ recording = true;
15025
+ clearTimeout(startWait);
15026
+ context.on("page", onPage);
15027
+ send({ type: "loaded" });
15028
+ };
15029
+ socket.on("connect", () => {
15030
+ attached = true;
15031
+ send({
15032
+ type: "attach",
15033
+ token: channel.token,
15034
+ file: place.file,
15035
+ line: place.line,
15036
+ reason: options.reason,
15037
+ testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
15038
+ baseURL: typeof use.baseURL === "string" ? use.baseURL : null
15039
+ });
15040
+ });
15041
+ socket.on(
15042
+ "data",
15043
+ lineReader(
15044
+ (line) => {
15045
+ const message = parseHandoffToTest(line);
15046
+ if (!message || settled) return;
15047
+ switch (message.type) {
15048
+ case "refused":
15049
+ return finish(message.message);
15050
+ case "load":
15051
+ return void load(message.bundle).catch(
15052
+ (error) => finish(`the recorder could not be loaded: ${error instanceof Error ? error.message : String(error)}`)
15053
+ );
15054
+ case "answer": {
15055
+ const answer = answers.get(message.call);
15056
+ answers.delete(message.call);
15057
+ return answer?.(message.value);
15058
+ }
15059
+ case "dispatch":
15060
+ return void page.evaluate(
15061
+ ([name, value]) => {
15062
+ const dispatch = globalThis[name];
15063
+ if (typeof dispatch === "function") dispatch(value);
15064
+ },
15065
+ [IDE_DISPATCH_GLOBAL, message.message]
15066
+ ).catch(() => void 0);
15067
+ case "stop":
15068
+ return finish(null);
15069
+ }
15070
+ },
15071
+ () => finish("the editor sent a message too long to read.")
15072
+ )
15073
+ );
15074
+ socket.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
15075
+ socket.on("close", () => finish("the editor closed the recording."));
15076
+ });
15077
+ }
15078
+
15079
+ // src/internal/capture/css-coverage.ts
15080
+ var crypto7 = __toESM(require("crypto"));
15081
+ var fs27 = __toESM(require("fs"));
15082
+
15083
+ // ../core/src/css-rules.ts
15084
+ var GROUPING = /* @__PURE__ */ new Set(["media", "supports", "layer", "container", "scope", "starting-style", "document"]);
15085
+ function parseCssRules(text) {
15086
+ const rules = [];
15087
+ const open = [];
15088
+ let preludeStart = -1;
15089
+ for (let i = 0; i < text.length; i++) {
15090
+ const c = text[i];
15091
+ if (c === "/" && text[i + 1] === "*") {
15092
+ const end = text.indexOf("*/", i + 2);
15093
+ i = end < 0 ? text.length : end + 1;
15094
+ continue;
15095
+ }
15096
+ if (c === '"' || c === "'") {
15097
+ if (preludeStart < 0) preludeStart = i;
15098
+ for (i++; i < text.length && text[i] !== c && text[i] !== "\n"; i++) if (text[i] === "\\") i++;
15099
+ continue;
15100
+ }
15101
+ if (c === "\\") {
15102
+ if (preludeStart < 0) preludeStart = i;
15103
+ i++;
15104
+ continue;
15105
+ }
15106
+ if ((c === "u" || c === "U") && /^url\(/i.test(text.slice(i, i + 4)) && !/[\w-]/.test(text[i - 1] ?? "")) {
15107
+ let j = i + 4;
15108
+ while (j < text.length && /\s/.test(text[j])) j++;
15109
+ if (text[j] !== '"' && text[j] !== "'") {
15110
+ if (preludeStart < 0) preludeStart = i;
15111
+ for (; j < text.length && text[j] !== ")"; j++) if (text[j] === "\\") j++;
15112
+ i = j;
15113
+ continue;
15114
+ }
15115
+ }
15116
+ if (text.startsWith("<!--", i) || text.startsWith("-->", i)) {
15117
+ i += text.startsWith("<!--", i) ? 3 : 2;
15118
+ continue;
15119
+ }
15120
+ if (c === "{") {
15121
+ const start = preludeStart < 0 ? i : preludeStart;
15122
+ const atRule = /^@([\w-]+)/.exec(text.slice(start, i));
15123
+ const parent = open[open.length - 1];
15124
+ const kind = atRule ? GROUPING.has(atRule[1].toLowerCase()) ? "group" : "other" : parent?.kind === "other" ? "other" : "style";
15125
+ open.push({ kind, start, open: i });
15126
+ preludeStart = -1;
15127
+ continue;
15128
+ }
15129
+ if (c === "}") {
15130
+ const block = open.pop();
15131
+ if (block && block.kind !== "other")
15132
+ rules.push({ kind: block.kind, start: block.start, open: block.open, end: i + 1 });
15133
+ preludeStart = -1;
15134
+ continue;
15135
+ }
15136
+ if (c === ";") {
15137
+ preludeStart = -1;
15138
+ continue;
15139
+ }
15140
+ if (preludeStart < 0 && !/\s/.test(c)) preludeStart = i;
15141
+ }
15142
+ for (const block of open) {
15143
+ if (block.kind !== "other")
15144
+ rules.push({ kind: block.kind, start: block.start, open: block.open, end: text.length });
15145
+ }
15146
+ return rules.sort((a, b) => a.start - b.start);
15147
+ }
15148
+ function usedCssRules(rules, used) {
15149
+ const styleAt = /* @__PURE__ */ new Map();
15150
+ const groups = [];
15151
+ for (const rule of rules) {
15152
+ if (rule.kind === "style") styleAt.set(rule.start, rule);
15153
+ else groups.push(rule);
15154
+ }
15155
+ const out = /* @__PURE__ */ new Map();
15156
+ for (const range of used) {
15157
+ const style = styleAt.get(range.startOffset);
15158
+ if (style) {
15159
+ out.set(`${style.start}`, [style.start, style.end]);
15160
+ continue;
15161
+ }
15162
+ const group = groups.find((rule) => rule.start <= range.startOffset && range.startOffset <= rule.open);
15163
+ if (group) out.set(`${group.start}`, [group.start, group.start + 1]);
15164
+ }
15165
+ return [...out.values()].sort((a, b) => a[0] - b[0]);
15166
+ }
15167
+ function allCssRules(rules) {
15168
+ return rules.map((rule) => rule.kind === "style" ? [rule.start, rule.end] : [rule.start, rule.start + 1]);
15169
+ }
15170
+ function cssRuleLines(text, offsets, options = {}) {
15171
+ const starts = lineStarts(text);
15172
+ const lineOffset = options.lineOffset ?? 0;
15173
+ const out = /* @__PURE__ */ new Map();
15174
+ const add2 = (source, range) => {
15175
+ const ranges = out.get(source);
15176
+ if (ranges) ranges.push(range);
15177
+ else out.set(source, [range]);
15178
+ };
15179
+ for (const [startOffset, endOffset] of offsets) {
15180
+ const start = offsetToPosition(starts, startOffset);
15181
+ const end = offsetToPosition(starts, Math.max(startOffset, endOffset - 1));
15182
+ if (!options.lookup) {
15183
+ add2(-1, [start.line + lineOffset + 1, end.line + lineOffset + 1]);
15184
+ continue;
15185
+ }
15186
+ const first = options.lookup.originalAt(start.line, start.column);
15187
+ if (!first) continue;
15188
+ const last = options.lookup.originalAt(end.line, end.column);
15189
+ const lastLine = last && last.source === first.source && last.line >= first.line ? last.line : first.line;
15190
+ add2(first.source, [first.line + 1, lastLine + 1]);
15191
+ }
15192
+ return out;
15193
+ }
15194
+
15195
+ // src/internal/capture/css-coverage.ts
15196
+ var STYLE_SOURCES = /\.(?:css|scss|sass|less|styl|stylus|pcss|postcss|vue|svelte|astro|html?)$/i;
15197
+ var BUILD_DIRS2 = /(?:^|\/)(?:dist|build|out|\.output|\.next|\.nuxt|\.svelte-kit)\//;
15198
+ var resolutions = /* @__PURE__ */ new Map();
15199
+ var MAX_CACHED_SHEETS = 2e3;
15200
+ function fileForStyleUrl(url, roots) {
15201
+ let pathname;
15202
+ try {
15203
+ const parsed2 = new URL(url);
15204
+ if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:" && parsed2.protocol !== "file:") return null;
15205
+ pathname = parsed2.pathname;
15206
+ } catch {
15207
+ return null;
15208
+ }
15209
+ return sourceFile(decodeURIComponent(pathname), roots);
15210
+ }
15211
+ function sourceFile(source, roots) {
15212
+ const bare = source.replace(/[?#].*$/, "");
15213
+ if (!STYLE_SOURCES.test(bare) || /(?:^|\/)node_modules\//.test(bare)) return null;
15214
+ const file = resolveSourcePath(bare, roots);
15215
+ if (!file || BUILD_DIRS2.test(file.replace(/\\/g, "/")) || /(?:^|[\\/])node_modules[\\/]/.test(file)) return null;
15216
+ return file;
15217
+ }
15218
+ function styleBlockLine(file, index) {
15219
+ let text;
15220
+ try {
15221
+ text = fs27.readFileSync(file, "utf-8");
15222
+ } catch {
15223
+ return 0;
15224
+ }
15225
+ const blocks = [...text.matchAll(/^<style\b[^>]*>/gim)];
15226
+ const block = blocks[index];
15227
+ if (!block) return 0;
15228
+ return text.slice(0, block.index + block[0].length).split("\n").length - 1;
15229
+ }
15230
+ async function sheetMap(sheet, text, fetchMap) {
15231
+ const ref = sourceMappingUrl(text) ?? sheet.sourceMapURL ?? null;
15232
+ if (!ref) return null;
15233
+ let raw;
15234
+ if (ref.startsWith("data:")) raw = decodeDataUrl(ref);
15235
+ else {
14198
15236
  try {
14199
- testInfo.setTimeout(rearmedTimeout(before, Date.now() - started));
15237
+ raw = await fetchMap(new URL(ref, sheet.sourceURL).href);
14200
15238
  } catch {
15239
+ raw = null;
14201
15240
  }
14202
15241
  }
15242
+ if (!raw) return null;
15243
+ try {
15244
+ return decodeSourceMap(JSON.parse(raw));
15245
+ } catch {
15246
+ return null;
15247
+ }
15248
+ }
15249
+ function sameLines(text, rules, file, lineOffset) {
15250
+ let source;
15251
+ try {
15252
+ source = fs27.readFileSync(file, "utf-8");
15253
+ } catch {
15254
+ return false;
15255
+ }
15256
+ const fileLines = source.split(/\r?\n/);
15257
+ const sheetLineStarts = [0];
15258
+ for (let i = 0; i < text.length; i++) if (text[i] === "\n") sheetLineStarts.push(i + 1);
15259
+ let low = 0;
15260
+ for (const rule of rules) {
15261
+ while (low + 1 < sheetLineStarts.length && sheetLineStarts[low + 1] <= rule.start) low++;
15262
+ const head = text.slice(rule.start, rule.open).split("\n")[0].trim();
15263
+ if (head && !(fileLines[low + lineOffset] ?? "").includes(head)) return false;
15264
+ }
15265
+ return true;
15266
+ }
15267
+ async function resolveSheet(sheet, text, dirs, fetchMap) {
15268
+ const lookup = await sheetMap(sheet, text, fetchMap);
15269
+ const rules = parseCssRules(text);
15270
+ if (lookup) {
15271
+ const sources = lookup.sources.map((source) => {
15272
+ const direct = sourceFile(source, dirs);
15273
+ if (direct) return direct;
15274
+ try {
15275
+ return /^[a-z][\w+.-]*:/i.test(source) ? null : fileForStyleUrl(new URL(source, sheet.sourceURL).href, dirs);
15276
+ } catch {
15277
+ return null;
15278
+ }
15279
+ });
15280
+ return { rules, lookup, file: null, lineOffset: 0, sources, exact: true };
15281
+ }
15282
+ if (sheet.devId) {
15283
+ const file2 = sourceFile(sheet.devId, dirs);
15284
+ if (!file2) return null;
15285
+ const index = /[?&]index=(\d+)/.exec(sheet.devId);
15286
+ const lineOffset2 = /\.(?:vue|svelte|astro)$/i.test(file2) && index ? styleBlockLine(file2, Number(index[1])) : 0;
15287
+ return { rules, lookup: null, file: file2, lineOffset: lineOffset2, sources: [], exact: sameLines(text, rules, file2, lineOffset2) };
15288
+ }
15289
+ const file = fileForStyleUrl(sheet.sourceURL, dirs);
15290
+ if (!file) return null;
15291
+ const lineOffset = sheet.isInline ? sheet.startLine : 0;
15292
+ return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text, rules, file, lineOffset) };
15293
+ }
15294
+ function cachedResolution(sheet, text, dirs, fetchMap) {
15295
+ const version = crypto7.createHash("sha1").update(text).digest("hex");
15296
+ const key = [sheet.devId ?? sheet.sourceURL, sheet.isInline ? sheet.startLine : "", dirs.join("\0"), version].join(
15297
+ " "
15298
+ );
15299
+ let pending = resolutions.get(key);
15300
+ if (!pending) {
15301
+ pending = resolveSheet(sheet, text, dirs, fetchMap).catch(() => null);
15302
+ resolutions.set(key, pending);
15303
+ if (resolutions.size > MAX_CACHED_SHEETS) resolutions.delete(resolutions.keys().next().value);
15304
+ }
15305
+ return pending;
15306
+ }
15307
+ function add(into, file, ranges, repoRoot, fileLevel = false) {
15308
+ if (ranges.length === 0 && !fileLevel) return;
15309
+ const key = relativeToRepo(file, repoRoot);
15310
+ into.set(key, [...into.get(key) ?? [], ...ranges]);
15311
+ }
15312
+ async function resolveCssCoverage(sheets, roots, fetchMap) {
15313
+ const reached = /* @__PURE__ */ new Map();
15314
+ const known = /* @__PURE__ */ new Map();
15315
+ for (const sheet of sheets) {
15316
+ if (sheet.text === void 0) continue;
15317
+ const dirs = scriptRoots(roots, sheet.sourceURL, sheet.pageUrl);
15318
+ const resolution = await cachedResolution(sheet, sheet.text, dirs, fetchMap);
15319
+ if (!resolution) continue;
15320
+ const used = usedCssRules(resolution.rules, [...sheet.used.values()]);
15321
+ if (!resolution.exact) {
15322
+ if (resolution.file && used.length > 0) add(reached, resolution.file, [], roots.repoRoot, true);
15323
+ continue;
15324
+ }
15325
+ const options = { lookup: resolution.lookup, lineOffset: resolution.lineOffset };
15326
+ const fileOf2 = (source) => source === -1 ? resolution.file : resolution.sources[source] ?? null;
15327
+ const matched = cssRuleLines(sheet.text, used, options);
15328
+ for (const [source, ranges] of matched) {
15329
+ const file = fileOf2(source);
15330
+ if (file) add(reached, file, ranges, roots.repoRoot);
15331
+ }
15332
+ for (const [source, ranges] of cssRuleLines(sheet.text, allCssRules(resolution.rules), options)) {
15333
+ const file = fileOf2(source);
15334
+ if (file) add(known, file, ranges, roots.repoRoot);
15335
+ }
15336
+ }
15337
+ return { reached, known };
14203
15338
  }
14204
15339
 
14205
15340
  // src/internal/capture/page-interaction.ts
@@ -14497,17 +15632,27 @@ function createSink() {
14497
15632
  pickOffered: false,
14498
15633
  userPick: null,
14499
15634
  pause: null,
15635
+ locatorCalls: /* @__PURE__ */ new Map(),
15636
+ stops: [],
15637
+ shownStops: /* @__PURE__ */ new Map(),
15638
+ released: [],
15639
+ releasedRequests: /* @__PURE__ */ new Set(),
15640
+ onReleasedRequestsEnded: null,
15641
+ releasedQuietSince: 0,
14500
15642
  locatorPages: new LocatorPageLog(),
14501
- codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
15643
+ codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
14502
15644
  serverCoverage: /* @__PURE__ */ new Map(),
14503
15645
  baseURL: null
14504
15646
  };
14505
15647
  }
14506
15648
  var MAX_CODE_REACH_PAGES = 16;
14507
15649
  var CODE_REACH_START_WAIT_MS = 2e3;
15650
+ function pageCoverageInWorker() {
15651
+ return browserCoverageInWorker() || cssCoverageInWorker();
15652
+ }
14508
15653
  function maybeStartCodeReach(page, alreadyOpen = false) {
14509
15654
  const sink = currentSink;
14510
- if (!sink || !browserCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
15655
+ if (!sink || !pageCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
14511
15656
  const known = sink.codeReach.sessions.get(page);
14512
15657
  if (known) return known.started;
14513
15658
  if (alreadyOpen) {
@@ -14520,9 +15665,9 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
14520
15665
  return started;
14521
15666
  }
14522
15667
  async function startCodeReachOnOpenPages() {
14523
- if (!browserCoverageInWorker() || OPEN_PAGES.size === 0) return;
15668
+ if (!pageCoverageInWorker() || OPEN_PAGES.size === 0) return;
14524
15669
  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())]);
15670
+ await Promise.race([started, new Promise((resolve11) => setTimeout(resolve11, CODE_REACH_START_WAIT_MS).unref())]);
14526
15671
  }
14527
15672
  function configuredCodeReachRoots() {
14528
15673
  const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
@@ -14536,7 +15681,7 @@ function configuredCodeReachRoots() {
14536
15681
  }
14537
15682
  } catch {
14538
15683
  }
14539
- return raw.split(path28.delimiter).filter(Boolean);
15684
+ return raw.split(path30.delimiter).filter(Boolean);
14540
15685
  }
14541
15686
  async function stopSinkCodeReach(sink, closing) {
14542
15687
  const stopping = [];
@@ -14563,30 +15708,36 @@ async function stopSinkCodeReach(sink, closing) {
14563
15708
  async function stopPageCodeReach(sink, page, session, roots) {
14564
15709
  try {
14565
15710
  if (!await session.started || isPageClosed(page)) return;
14566
- const entries = await stopCodeReach(page);
14567
- if (!entries) return;
14568
- const { reached, known } = await resolveCodeReachFunctions(entries, roots, pageMapFetcher(page), page.url(), {
15711
+ const { scripts, sheets } = await stopPageCoverage(page);
15712
+ const fetchMap = pageMapFetcher(page);
15713
+ const append = (into, from) => {
15714
+ for (const [file, ranges] of from) into.set(file, [...into.get(file) ?? [], ...ranges]);
15715
+ };
15716
+ if (sheets.length > 0) {
15717
+ const rules = await resolveCssCoverage(sheets, roots, fetchMap);
15718
+ append(sink.codeReach.cssFiles, rules.reached);
15719
+ append(sink.codeReach.cssKnown, rules.known);
15720
+ }
15721
+ if (!scripts) return;
15722
+ const { reached, known } = await resolveCodeReachFunctions(scripts, roots, fetchMap, page.url(), {
14569
15723
  known: browserFunctionsInWorker()
14570
15724
  });
14571
- for (const [file, functions] of reached) {
14572
- sink.codeReach.files.set(file, [...sink.codeReach.files.get(file) ?? [], ...functions]);
14573
- }
14574
- for (const [file, functions] of known) {
14575
- sink.codeReach.known.set(file, [...sink.codeReach.known.get(file) ?? [], ...functions]);
14576
- }
15725
+ append(sink.codeReach.files, reached);
15726
+ append(sink.codeReach.known, known);
14577
15727
  } catch {
14578
15728
  }
14579
15729
  }
14580
15730
  var currentSink = null;
14581
15731
  var KNOWN_FUNCTIONS_SENT = /* @__PURE__ */ new Map();
15732
+ var KNOWN_CSS_RULES_SENT = /* @__PURE__ */ new Map();
14582
15733
  var PENDING_PROBES = /* @__PURE__ */ new Set();
14583
15734
  async function drainPendingProbes(capMs) {
14584
15735
  if (PENDING_PROBES.size === 0) return;
14585
15736
  let cap;
14586
15737
  await Promise.race([
14587
15738
  Promise.allSettled(PENDING_PROBES),
14588
- new Promise((resolve9) => {
14589
- cap = setTimeout(resolve9, capMs);
15739
+ new Promise((resolve11) => {
15740
+ cap = setTimeout(resolve11, capMs);
14590
15741
  })
14591
15742
  ]);
14592
15743
  clearTimeout(cap);
@@ -14626,10 +15777,10 @@ async function readWebVitals(page) {
14626
15777
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
14627
15778
  paint[key] = Math.round(entry.startTime);
14628
15779
  }
14629
- const readBuffered = (type, extra) => new Promise((resolve9) => {
15780
+ const readBuffered = (type, extra) => new Promise((resolve11) => {
14630
15781
  try {
14631
15782
  const PO = globalThis.PerformanceObserver;
14632
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve9(null);
15783
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve11(null);
14633
15784
  const out = [];
14634
15785
  const po = new PO((list) => out.push(...list.getEntries()));
14635
15786
  po.observe({ type, buffered: true, ...extra });
@@ -14639,10 +15790,10 @@ async function readWebVitals(page) {
14639
15790
  po.disconnect();
14640
15791
  } catch {
14641
15792
  }
14642
- resolve9(out);
15793
+ resolve11(out);
14643
15794
  }, 0);
14644
15795
  } catch {
14645
- resolve9(null);
15796
+ resolve11(null);
14646
15797
  }
14647
15798
  });
14648
15799
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -14757,12 +15908,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
14757
15908
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
14758
15909
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
14759
15910
  function withTimeout(promise, ms) {
14760
- return new Promise((resolve9, reject) => {
15911
+ return new Promise((resolve11, reject) => {
14761
15912
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
14762
15913
  promise.then(
14763
15914
  (value) => {
14764
15915
  clearTimeout(timer);
14765
- resolve9(value);
15916
+ resolve11(value);
14766
15917
  },
14767
15918
  (error) => {
14768
15919
  clearTimeout(timer);
@@ -14877,6 +16028,7 @@ async function maybeOpenPicker(sink, closing) {
14877
16028
  var INSTRUMENTED_PAGES = /* @__PURE__ */ new WeakSet();
14878
16029
  var OPEN_PAGES = /* @__PURE__ */ new Set();
14879
16030
  var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
16031
+ var OPEN_CONTEXTS = /* @__PURE__ */ new Set();
14880
16032
  var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
14881
16033
  var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
14882
16034
  var ACTION_METHOD_SET = new Set(ACTION_METHODS);
@@ -14997,8 +16149,20 @@ function recordLocatorPage(sink, page, locator, location, arrival) {
14997
16149
  } catch {
14998
16150
  }
14999
16151
  }
15000
- function pausesAt(pause, callerLocation) {
15001
- return !pause.finished && (pause.next || pause.set.matches(callerLocation));
16152
+ function knowLocatorCall(sink, page, target, location, origin) {
16153
+ if (!sink?.pause || !location) return null;
16154
+ let settle = () => {
16155
+ };
16156
+ const settled = new Promise((resolve11) => settle = resolve11);
16157
+ const call = { page, target, location, origin, picked: false, settled, settle };
16158
+ sink.locatorCalls.set(callSiteLine(location), call);
16159
+ return call;
16160
+ }
16161
+ function forgetLocatorCall(sink, call) {
16162
+ if (!sink || !call) return;
16163
+ call.settle();
16164
+ const key = callSiteLine(call.location);
16165
+ if (sink.locatorCalls.get(key) === call) sink.locatorCalls.delete(key);
15002
16166
  }
15003
16167
  function locatorSource(target, method, args) {
15004
16168
  try {
@@ -15008,42 +16172,220 @@ function locatorSource(target, method, args) {
15008
16172
  }
15009
16173
  return renderFailing({ method, args });
15010
16174
  }
15011
- async function pauseBefore(sink, page, target, callerLocation, action, origin) {
15012
- const pause = sink.pause;
16175
+ var PAGE_PAUSES = /* @__PURE__ */ new WeakMap();
16176
+ var PLAYWRIGHT_PAUSES = /* @__PURE__ */ new WeakMap();
16177
+ async function armBreakpoints(contexts) {
16178
+ const state = currentSink?.pause;
16179
+ if (!state || state.finished || state.set.size === 0) return;
16180
+ await Promise.all(
16181
+ [...contexts].map(
16182
+ (context) => context && watchStops(context, onDebuggerStop) ? steerDebugger(context, "pause") : void 0
16183
+ )
16184
+ );
16185
+ }
16186
+ function onDebuggerStop(context) {
16187
+ const details = contextDebugger(context)?.pausedDetails();
16188
+ if (!details || PLAYWRIGHT_PAUSES.has(context)) return;
16189
+ const sink = currentSink;
16190
+ const state = sink?.pause ?? null;
16191
+ void (async () => {
16192
+ let how = "resume";
16193
+ try {
16194
+ if (!sink || !state) return;
16195
+ const callSite = stopSite(sink, details.location);
16196
+ const pagePause = PAGE_PAUSES.get(context);
16197
+ const decided = pagePause ? { show: "page-pause" } : atStop(state, callSite);
16198
+ if ("go" in decided) {
16199
+ how = decided.go;
16200
+ return;
16201
+ }
16202
+ how = goOn(state, await showStop(sink, context, details.title, callSite ?? pagePause, decided.show));
16203
+ } catch {
16204
+ if (state) how = goOn(state);
16205
+ } finally {
16206
+ await steerDebugger(context, how);
16207
+ }
16208
+ })();
16209
+ }
16210
+ function stopSite(sink, location) {
16211
+ if (path30.dirname(path30.resolve(location.file)) !== __dirname) return stopCallSite(location);
16212
+ return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
16213
+ }
16214
+ function stopPage(sink, context, call) {
16215
+ for (const page of [call?.page, sink.lastActivePage]) {
16216
+ if (page && !isPageClosed(page) && pageContext(page) === context) return page;
16217
+ }
16218
+ const open = context.pages().filter((page) => !isPageClosed(page));
16219
+ return open[open.length - 1] ?? null;
16220
+ }
16221
+ async function showStop(sink, context, title, location, reason) {
15013
16222
  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 },
16223
+ const call = sink.locatorCalls.get(callSiteLine(location));
16224
+ const page = stopPage(sink, context, call);
16225
+ if (!testInfo || !page) return void 0;
16226
+ const onPick = async (picked) => {
16227
+ if (call) {
16228
+ call.picked = true;
16229
+ const { method, args } = call.origin;
16230
+ const rendered = locatorSource(call.target, method, args);
16231
+ applyPickToSnapshots(sink.capturedLocators, {
16232
+ failing: { method, args, rendered, location: call.location },
15021
16233
  ...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(
16234
+ });
16235
+ sink.probedLocations.add(call.location);
16236
+ }
16237
+ console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
16238
+ await postPickToEditor(process.env[PIWI_LOCAL_DEBUG_ENV.editorSend], pickPayload(picked.picked.locator, location));
16239
+ };
16240
+ const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
16241
+ sink.stops.push(record);
16242
+ const end = new AbortController();
16243
+ const shown = withTestTimeoutHeld(
16244
+ testInfo,
16245
+ () => internalCall(
15033
16246
  page,
15034
16247
  () => pauseHere(
15035
16248
  page,
15036
- testInfo,
15037
- { location: callerLocation, action, locator, target },
16249
+ {
16250
+ location,
16251
+ call: title,
16252
+ attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
16253
+ target: call?.page === page ? call.target : null
16254
+ },
15038
16255
  { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
15039
- onPick
16256
+ onPick,
16257
+ recorderAt(page, testInfo, location, reason === "page-pause" ? "pause" : "breakpoint"),
16258
+ end.signal
15040
16259
  )
15041
- );
15042
- if (choice === "step") pause.next = true;
15043
- else if (choice === "finish") pause.finished = true;
15044
- } catch {
16260
+ )
16261
+ ).finally(() => {
16262
+ if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
16263
+ else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
16264
+ sink.shownStops.delete(end);
16265
+ });
16266
+ const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
16267
+ sink.shownStops.set(end, { page, record, done: shown.catch(() => void 0), heldAt });
16268
+ const answer = await shown;
16269
+ if (!end.signal.aborted) return answer;
16270
+ return end.signal.reason === "closed" ? "resume" : "finish";
16271
+ }
16272
+ var END_STOP_MS = 2e3;
16273
+ var RELEASED_CALL_MS = 2e3;
16274
+ var RELEASED_QUIET_MS = 200;
16275
+ async function endShownStops(sink, why, closing) {
16276
+ const ending = [];
16277
+ let heldAt = Infinity;
16278
+ for (const [end, { page, record, done, heldAt: stopHeldAt }] of sink.shownStops) {
16279
+ if (closing && closing.page !== page && (!closing.context || pageContext(page) !== closing.context)) continue;
16280
+ if (!end.signal.aborted) {
16281
+ record.duration = Date.now() - record.startedAt;
16282
+ end.abort(why);
16283
+ if (why === "ended" && stopHeldAt !== null) heldAt = Math.min(heldAt, stopHeldAt);
16284
+ }
16285
+ ending.push(done);
16286
+ }
16287
+ if (sink.testInfo && heldAt !== Infinity) restoreTestTime(sink.testInfo, heldAt);
16288
+ await settledWithin(ending, END_STOP_MS);
16289
+ }
16290
+ function releaseCall(sink, settled) {
16291
+ sink.releasedQuietSince = Date.now();
16292
+ sink.released.push(settled.then(() => void (sink.releasedQuietSince = Date.now())));
16293
+ }
16294
+ function releasedRequestsEnded(sink) {
16295
+ if (sink.releasedRequests.size === 0) return Promise.resolve();
16296
+ return new Promise((resolve11) => sink.onReleasedRequestsEnded = resolve11);
16297
+ }
16298
+ async function awaitReleasedCalls(sink) {
16299
+ if (sink.released.length === 0) return;
16300
+ const deadline = Date.now() + RELEASED_CALL_MS;
16301
+ await settledWithin(sink.released, RELEASED_CALL_MS);
16302
+ for (let now = Date.now(); now < deadline; now = Date.now()) {
16303
+ if (sink.releasedRequests.size > 0) {
16304
+ await settledWithin([releasedRequestsEnded(sink)], deadline - now);
16305
+ continue;
16306
+ }
16307
+ const quiet = now - sink.releasedQuietSince;
16308
+ if (quiet >= RELEASED_QUIET_MS) return;
16309
+ await new Promise((resolve11) => setTimeout(resolve11, Math.min(RELEASED_QUIET_MS - quiet, deadline - now)));
16310
+ }
16311
+ }
16312
+ async function settledWithin(pending, ms) {
16313
+ if (pending.length === 0) return;
16314
+ let timer;
16315
+ await Promise.race([Promise.allSettled(pending), new Promise((resolve11) => timer = setTimeout(resolve11, ms))]);
16316
+ clearTimeout(timer);
16317
+ }
16318
+ function endStopsWithTestFunction(testInfo, sink) {
16319
+ const callbacks = testInfo._onDidFinishTestFunctionCallbacks;
16320
+ if (!(callbacks instanceof Set)) return;
16321
+ const theirs = [...callbacks];
16322
+ callbacks.clear();
16323
+ callbacks.add(() => endShownStops(sink, "ended"));
16324
+ for (const callback of theirs) callbacks.add(callback);
16325
+ }
16326
+ function recorderAt(page, testInfo, location, reason) {
16327
+ if (!canRecordFromHere()) return void 0;
16328
+ return async (ended) => {
16329
+ console.log(`[piwi] Recording from ${callSiteLine(location)}: the editor writes the steps.`);
16330
+ const why = await recordFromHere({ page, testInfo, location, reason, ended });
16331
+ if (ended?.aborted) console.log("[piwi] The recording ended with the test.");
16332
+ else
16333
+ console.log(
16334
+ why ? `[piwi] Nothing was recorded: ${why}` : "[piwi] The recording ended: the test is paused again."
16335
+ );
16336
+ };
16337
+ }
16338
+ async function pauseInPlace(page, playwrightPause, location) {
16339
+ const sink = currentSink;
16340
+ const testInfo = sink?.testInfo;
16341
+ const context = pageContext(page);
16342
+ if (!sink || !testInfo || !location || !context || !contextDebugger(context)) {
16343
+ return leaveToPlaywright(context, playwrightPause);
16344
+ }
16345
+ if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
16346
+ return leaveToPlaywright(context, playwrightPause);
16347
+ }
16348
+ sink.pause ??= { set: NO_BREAKPOINTS, step: false, finished: false };
16349
+ watchStops(context, onDebuggerStop);
16350
+ await steerDebugger(context, "pause");
16351
+ PAGE_PAUSES.set(context, location);
16352
+ try {
16353
+ await playwrightPause({ __testHookKeepTestTimeout: true });
16354
+ } finally {
16355
+ PAGE_PAUSES.delete(context);
15045
16356
  }
15046
16357
  }
16358
+ async function leaveToPlaywright(context, playwrightPause) {
16359
+ if (context) PLAYWRIGHT_PAUSES.set(context, (PLAYWRIGHT_PAUSES.get(context) ?? 0) + 1);
16360
+ try {
16361
+ await withInspector(() => playwrightPause());
16362
+ } finally {
16363
+ if (context) {
16364
+ const left = (PLAYWRIGHT_PAUSES.get(context) ?? 1) - 1;
16365
+ if (left > 0) PLAYWRIGHT_PAUSES.set(context, left);
16366
+ else PLAYWRIGHT_PAUSES.delete(context);
16367
+ }
16368
+ }
16369
+ }
16370
+ var PLAYWRIGHT_PAUSE = /* @__PURE__ */ Symbol.for("@piwitests/reporter:playwright-pause");
16371
+ function playwrightPauseOf(page) {
16372
+ const current = page.pause;
16373
+ if (typeof current !== "function") return null;
16374
+ const own = current[PLAYWRIGHT_PAUSE] ?? current;
16375
+ const proto = Object.getPrototypeOf(page);
16376
+ const theirs = proto?.pause;
16377
+ if (proto && typeof theirs === "function") {
16378
+ if (!theirs[PLAYWRIGHT_PAUSE]) {
16379
+ const wrapped = function(options) {
16380
+ return leaveToPlaywright(pageContext(this), () => theirs.call(this, options));
16381
+ };
16382
+ wrapped[PLAYWRIGHT_PAUSE] = theirs;
16383
+ proto.pause = wrapped;
16384
+ }
16385
+ markPausesWrapped();
16386
+ }
16387
+ return (options) => own.call(page, options);
16388
+ }
15047
16389
  function wrapLocator(page, locator, originMethod, originArgs) {
15048
16390
  return new Proxy(locator, {
15049
16391
  get(target, prop) {
@@ -15065,36 +16407,35 @@ function wrapLocator(page, locator, originMethod, originArgs) {
15065
16407
  const isNot = Boolean(callArgs[1]?.isNot);
15066
16408
  const callerLocation = sink ? captureCallerLocation() : null;
15067
16409
  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), {
16410
+ const call = knowLocatorCall(sink, page, target, callerLocation, { method: originMethod, args: originArgs });
16411
+ try {
16412
+ if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
16413
+ return await callMethod(target, prop, callArgs);
16414
+ }
16415
+ sink.lastActivePage = page;
16416
+ const used = {
15070
16417
  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 });
16418
+ args: originArgs,
16419
+ raw: `${originMethod}(${JSON.stringify(originArgs)})`
16420
+ };
16421
+ const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
16422
+ let seq = -1;
16423
+ if (!alreadyProbed) {
16424
+ seq = sink.capturedLocators.length;
16425
+ sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
16426
+ }
16427
+ const result = await callMethod(target, prop, callArgs);
16428
+ const matches = result?.matches;
16429
+ if (matches === true && !alreadyProbed && !call?.picked) {
16430
+ if (callerLocation) sink.probedLocations.add(callerLocation);
16431
+ startElementCapture(sink, page, target, seq, callerLocation, used);
16432
+ } else if (matches === false) {
16433
+ sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
16434
+ }
16435
+ return result;
16436
+ } finally {
16437
+ forgetLocatorCall(sink, call);
15096
16438
  }
15097
- return result;
15098
16439
  };
15099
16440
  }
15100
16441
  if (!ACTION_METHOD_SET.has(prop)) return original;
@@ -15109,9 +16450,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
15109
16450
  args: originArgs,
15110
16451
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
15111
16452
  };
15112
- if (sink.pause && pausesAt(sink.pause, callerLocation)) {
15113
- await pauseBefore(sink, page, target, callerLocation, String(prop), used);
15114
- }
16453
+ const call = knowLocatorCall(sink, page, target, callerLocation, used);
15115
16454
  const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
15116
16455
  let seq = -1;
15117
16456
  if (!alreadyProbed) {
@@ -15129,8 +16468,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
15129
16468
  } catch (error) {
15130
16469
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
15131
16470
  throw error;
16471
+ } finally {
16472
+ forgetLocatorCall(sink, call);
15132
16473
  }
15133
- if (!alreadyProbed) {
16474
+ if (!alreadyProbed && !call?.picked) {
15134
16475
  if (callerLocation) sink.probedLocations.add(callerLocation);
15135
16476
  startElementCapture(sink, page, target, seq, callerLocation, used);
15136
16477
  }
@@ -15183,12 +16524,17 @@ function instrumentPage(page) {
15183
16524
  }
15184
16525
  const ctx = pageContext(page);
15185
16526
  if (ctx) instrumentContext(ctx);
16527
+ const playwrightPause = playwrightPauseOf(page);
16528
+ if (playwrightPause) {
16529
+ page.pause = () => pauseInPlace(page, playwrightPause, captureCallerLocation());
16530
+ }
15186
16531
  if (typeof page.close === "function") {
15187
16532
  const originalClose = page.close.bind(page);
15188
16533
  page.close = async (...args) => {
15189
16534
  await drainPendingProbes(1e3);
15190
16535
  const sink = currentSink;
15191
16536
  if (sink) {
16537
+ await endShownStops(sink, "closed", { page });
15192
16538
  await stashPageState(sink, { page });
15193
16539
  await maybeOpenPicker(sink, { page });
15194
16540
  await stopSinkCodeReach(sink, { page });
@@ -15277,8 +16623,18 @@ function instrumentPage(page) {
15277
16623
  } catch {
15278
16624
  }
15279
16625
  });
16626
+ page.on("request", (request3) => {
16627
+ const sink = currentSink;
16628
+ if (!sink || sink.released.length === 0) return;
16629
+ sink.releasedRequests.add(request3);
16630
+ sink.releasedQuietSince = Date.now();
16631
+ });
15280
16632
  const onRequestEnd = (request3, failed) => {
15281
16633
  const sink = currentSink;
16634
+ if (sink?.releasedRequests.delete(request3)) {
16635
+ sink.releasedQuietSince = Date.now();
16636
+ if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
16637
+ }
15282
16638
  if (!sink || RECORDED_OPEN.has(request3)) return;
15283
16639
  sink.lastActivePage = page;
15284
16640
  const response = sink.openResponses.get(request3) ?? null;
@@ -15384,6 +16740,10 @@ async function awaitFlakeConditions() {
15384
16740
  function instrumentContext(context) {
15385
16741
  if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
15386
16742
  INSTRUMENTED_CONTEXTS.add(context);
16743
+ if (typeof context.once === "function") {
16744
+ OPEN_CONTEXTS.add(context);
16745
+ context.once("close", () => OPEN_CONTEXTS.delete(context));
16746
+ }
15387
16747
  observeOwnApiRequests(context);
15388
16748
  if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
15389
16749
  void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
@@ -15403,6 +16763,7 @@ function instrumentContext(context) {
15403
16763
  await drainPendingProbes(1e3);
15404
16764
  const sink = currentSink;
15405
16765
  if (sink) {
16766
+ await endShownStops(sink, "closed", { context });
15406
16767
  await stashPageState(sink, { context });
15407
16768
  await maybeOpenPicker(sink, { context });
15408
16769
  await stopSinkCodeReach(sink, { context });
@@ -15419,6 +16780,7 @@ function patchBrowser(browser) {
15419
16780
  browser.newPage = async (...args) => {
15420
16781
  const page = await originalNewPage(...args);
15421
16782
  instrumentPage(page);
16783
+ await armBreakpoints([pageContext(page)]);
15422
16784
  await awaitFlakeConditions();
15423
16785
  await maybeStartCodeReach(page);
15424
16786
  return page;
@@ -15427,6 +16789,7 @@ function patchBrowser(browser) {
15427
16789
  browser.newContext = async (...args) => {
15428
16790
  const context = await originalNewContext(...args);
15429
16791
  instrumentContext(context);
16792
+ await armBreakpoints([context]);
15430
16793
  return context;
15431
16794
  };
15432
16795
  if (typeof browser.close === "function") {
@@ -15478,14 +16841,20 @@ async function flushSink(sink, testInfo) {
15478
16841
  let drainDeadline;
15479
16842
  await Promise.race([
15480
16843
  Promise.allSettled(sink.capturePromises),
15481
- new Promise((resolve9) => {
15482
- drainDeadline = setTimeout(resolve9, 2e3);
16844
+ new Promise((resolve11) => {
16845
+ drainDeadline = setTimeout(resolve11, 2e3);
15483
16846
  })
15484
16847
  ]);
15485
16848
  clearTimeout(drainDeadline);
15486
16849
  const page = sink.lastActivePage;
15487
16850
  const pageReadable = page !== null && !isPageClosed(page);
15488
16851
  if (pageReadable) await maybeOpenPicker(sink);
16852
+ if (sink.stops.length > 0) {
16853
+ await testInfo.attach(ATTACHMENT_NAMES.pauses, {
16854
+ contentType: "application/json",
16855
+ body: Buffer.from(JSON.stringify(sink.stops))
16856
+ });
16857
+ }
15489
16858
  if (sink.capturedLocators.length > 0) {
15490
16859
  await testInfo.attach(ATTACHMENT_NAMES.locators, {
15491
16860
  contentType: "application/json",
@@ -15516,6 +16885,21 @@ async function flushSink(sink, testInfo) {
15516
16885
  body: Buffer.from(JSON.stringify(part))
15517
16886
  });
15518
16887
  }
16888
+ if (cssCoverageInWorker()) {
16889
+ const rules = sink.codeReach.cssFiles.size > 0 ? browserFunctionCoverage(sink.codeReach.cssFiles) : null;
16890
+ if (rules) {
16891
+ await testInfo.attach(ATTACHMENT_NAMES.cssCoverage, {
16892
+ contentType: "application/json",
16893
+ body: Buffer.from(JSON.stringify(rules))
16894
+ });
16895
+ }
16896
+ for (const part of newKnownFunctions(sink.codeReach.cssKnown, KNOWN_CSS_RULES_SENT)) {
16897
+ await testInfo.attach(ATTACHMENT_NAMES.cssRules, {
16898
+ contentType: "application/json",
16899
+ body: Buffer.from(JSON.stringify(part))
16900
+ });
16901
+ }
16902
+ }
15519
16903
  if (sink.serverCoverage.size > 0) {
15520
16904
  const requests = {};
15521
16905
  for (const [origin, ids] of sink.serverCoverage) requests[origin] = [...ids];
@@ -15686,6 +17070,7 @@ var piwiFixtures = {
15686
17070
  // never double-wraps a page the browser patch already instrumented.
15687
17071
  page: async ({ page }, use) => {
15688
17072
  instrumentPage(page);
17073
+ await armBreakpoints([pageContext(page)]);
15689
17074
  await awaitFlakeConditions();
15690
17075
  await maybeStartCodeReach(page);
15691
17076
  await use(page);
@@ -15706,8 +17091,9 @@ var piwiFixtures = {
15706
17091
  if (pauseSet.size) {
15707
17092
  const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
15708
17093
  if (reason) logPauseSkipOnce(reason);
15709
- else sink.pause = { set: pauseSet, next: false, finished: false };
17094
+ else sink.pause = { set: pauseSet, step: false, finished: false };
15710
17095
  }
17096
+ if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
15711
17097
  if (isFlakeMode()) {
15712
17098
  const plan = loadFlakePlan();
15713
17099
  const role = flakeRoleForTest(plan, {
@@ -15732,9 +17118,12 @@ var piwiFixtures = {
15732
17118
  activeResourceLedger()?.testStarted(testInfo);
15733
17119
  currentSink = sink;
15734
17120
  try {
17121
+ await armBreakpoints(OPEN_CONTEXTS);
15735
17122
  await startCodeReachOnOpenPages();
15736
17123
  await use();
15737
17124
  } finally {
17125
+ await endShownStops(sink, "ended");
17126
+ await awaitReleasedCalls(sink);
15738
17127
  currentSink = null;
15739
17128
  if (sink.probeItem) {
15740
17129
  const applied = sink.probeInterception?.applied() ?? false;
@@ -15792,13 +17181,13 @@ function extendPiwiFixtures(test) {
15792
17181
  }
15793
17182
 
15794
17183
  // src/internal/ai/ai-fixtures.ts
15795
- var fs26 = __toESM(require("fs"));
15796
- var path31 = __toESM(require("path"));
17184
+ var fs29 = __toESM(require("fs"));
17185
+ var path33 = __toESM(require("path"));
15797
17186
  var import_test = require("@playwright/test");
15798
17187
 
15799
17188
  // src/internal/ai/artifact.ts
15800
- var fs25 = __toESM(require("fs"));
15801
- var path29 = __toESM(require("path"));
17189
+ var fs28 = __toESM(require("fs"));
17190
+ var path31 = __toESM(require("path"));
15802
17191
  var ARTIFACT_VERSION = 1;
15803
17192
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
15804
17193
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -15891,7 +17280,7 @@ function parseEntry(text) {
15891
17280
  function readEntry(file) {
15892
17281
  let text;
15893
17282
  try {
15894
- text = fs25.readFileSync(file, "utf8");
17283
+ text = fs28.readFileSync(file, "utf8");
15895
17284
  } catch (error) {
15896
17285
  if (error.code === "ENOENT") return null;
15897
17286
  throw error;
@@ -15901,28 +17290,28 @@ function readEntry(file) {
15901
17290
  function writeEntry(file, entry) {
15902
17291
  const canonical = serializeEntry(entry);
15903
17292
  return withEntryLock(file, () => {
15904
- if (fs25.existsSync(file) && fs25.readFileSync(file, "utf8") === canonical) return { written: false };
15905
- fs25.mkdirSync(path29.dirname(file), { recursive: true });
17293
+ if (fs28.existsSync(file) && fs28.readFileSync(file, "utf8") === canonical) return { written: false };
17294
+ fs28.mkdirSync(path31.dirname(file), { recursive: true });
15906
17295
  const tmp = `${file}.${process.pid}.tmp`;
15907
- fs25.writeFileSync(tmp, canonical);
15908
- fs25.renameSync(tmp, file);
17296
+ fs28.writeFileSync(tmp, canonical);
17297
+ fs28.renameSync(tmp, file);
15909
17298
  return { written: true };
15910
17299
  });
15911
17300
  }
15912
17301
  function withEntryLock(file, fn) {
15913
17302
  const lock = `${file}.lock`;
15914
- fs25.mkdirSync(path29.dirname(file), { recursive: true });
17303
+ fs28.mkdirSync(path31.dirname(file), { recursive: true });
15915
17304
  const deadline = Date.now() + 5e3;
15916
17305
  for (; ; ) {
15917
17306
  try {
15918
- const fd = fs25.openSync(lock, "wx");
15919
- fs25.closeSync(fd);
17307
+ const fd = fs28.openSync(lock, "wx");
17308
+ fs28.closeSync(fd);
15920
17309
  break;
15921
17310
  } catch (error) {
15922
17311
  if (error.code !== "EEXIST") throw error;
15923
17312
  if (Date.now() > deadline) {
15924
17313
  try {
15925
- fs25.unlinkSync(lock);
17314
+ fs28.unlinkSync(lock);
15926
17315
  } catch {
15927
17316
  }
15928
17317
  }
@@ -15932,35 +17321,35 @@ function withEntryLock(file, fn) {
15932
17321
  return fn();
15933
17322
  } finally {
15934
17323
  try {
15935
- fs25.unlinkSync(lock);
17324
+ fs28.unlinkSync(lock);
15936
17325
  } catch {
15937
17326
  }
15938
17327
  }
15939
17328
  }
15940
17329
 
15941
17330
  // src/internal/ai/keys.ts
15942
- var crypto7 = __toESM(require("crypto"));
15943
- var path30 = __toESM(require("path"));
17331
+ var crypto8 = __toESM(require("crypto"));
17332
+ var path32 = __toESM(require("path"));
15944
17333
  var DEFAULT_AI_DIR = "__piwi__";
15945
17334
  function normalizeTemplate(template) {
15946
17335
  return template.trim().replace(/\s+/g, " ").toLowerCase();
15947
17336
  }
15948
17337
  function hashTemplate(normalized, ordinal = 0) {
15949
17338
  const input = ordinal > 0 ? `${normalized}#${ordinal}` : normalized;
15950
- return crypto7.createHash("sha256").update(input).digest("hex").slice(0, 8);
17339
+ return crypto8.createHash("sha256").update(input).digest("hex").slice(0, 8);
15951
17340
  }
15952
17341
  function slug(text, maxLength = 40) {
15953
17342
  const base = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
15954
17343
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
15955
17344
  }
15956
17345
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
15957
- return path30.join(path30.dirname(specFile), dir, path30.basename(specFile));
17346
+ return path32.join(path32.dirname(specFile), dir, path32.basename(specFile));
15958
17347
  }
15959
17348
  function entryPath(params) {
15960
17349
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
15961
17350
  const hash = hashTemplate(basis, params.ordinal ?? 0);
15962
17351
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
15963
- return path30.join(entryDir(params.specFile, params.dir), name);
17352
+ return path32.join(entryDir(params.specFile, params.dir), name);
15964
17353
  }
15965
17354
  function parseLocation2(location) {
15966
17355
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -16563,18 +17952,18 @@ function testIdentity(testInfo) {
16563
17952
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
16564
17953
  }
16565
17954
  function relativeToCwd(file) {
16566
- return path31.relative(process.cwd(), file).split(path31.sep).join("/");
17955
+ return path33.relative(process.cwd(), file).split(path33.sep).join("/");
16567
17956
  }
16568
17957
  function recordIntents(intents, entry) {
16569
- const add = (locator, kind) => {
17958
+ const add2 = (locator, kind) => {
16570
17959
  intents.set(`${entry.template}\0${locator}`, { template: entry.template, locator, kind });
16571
17960
  };
16572
17961
  if (entry.kind === "locator") {
16573
- add(locatorSource2(entry.locator), "locator");
17962
+ add2(locatorSource2(entry.locator), "locator");
16574
17963
  return;
16575
17964
  }
16576
- for (const step of entry.steps) add(locatorSource2(step.locator), "run");
16577
- if (entry.postcondition.locator) add(locatorSource2(entry.postcondition.locator), "run");
17965
+ for (const step of entry.steps) add2(locatorSource2(step.locator), "run");
17966
+ if (entry.postcondition.locator) add2(locatorSource2(entry.postcondition.locator), "run");
16578
17967
  }
16579
17968
  function missMessage(template, testTitle, relPath) {
16580
17969
  return [
@@ -16590,7 +17979,7 @@ function createAiApi(page, testInfo, config, used, intents) {
16590
17979
  const readSource = () => {
16591
17980
  if (source === null) {
16592
17981
  try {
16593
- source = fs26.readFileSync(testInfo.file, "utf8");
17982
+ source = fs29.readFileSync(testInfo.file, "utf8");
16594
17983
  } catch {
16595
17984
  source = "";
16596
17985
  }