@piwitests/reporter 0.63.0 → 0.64.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
@@ -255,7 +255,19 @@ var PIWI_FLAKE_ENV = {
255
255
  var PIWI_RESOURCES_RESULTS_ENV = "PIWI_RESOURCES_RESULTS";
256
256
  function readBool(val) {
257
257
  if (val === void 0) return void 0;
258
- return val === "true";
258
+ return ["true", "1", "yes", "on"].includes(val.trim().toLowerCase());
259
+ }
260
+ function normalizeBoolEnv(env = process.env) {
261
+ const keys = [
262
+ ...ENV_FALLBACK_SPECS.filter((spec) => spec.kind === "bool").map((spec) => spec.env),
263
+ PIWI_ENV_KEYS.verbose,
264
+ PIWI_ENV_KEYS.aiScreenshotFallback,
265
+ "PIWI_AI_UPDATE"
266
+ ];
267
+ for (const key of keys) {
268
+ const value = env[key];
269
+ if (value !== void 0) env[key] = readBool(value) ? "true" : "false";
270
+ }
259
271
  }
260
272
  var ENV_FALLBACK_SPECS = [
261
273
  { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
@@ -374,7 +386,7 @@ function resolveOptions(raw, configDir) {
374
386
  externalId.onDuplicate = externalIdOnDuplicate;
375
387
  if (Object.keys(externalId).length > 0) mergedRaw.externalId = externalId;
376
388
  const opts = { ...DEFAULTS, ...mergedRaw };
377
- if (env[PIWI_ENV_KEYS.verbose] !== void 0) opts.verbose = env[PIWI_ENV_KEYS.verbose] === "true";
389
+ if (env[PIWI_ENV_KEYS.verbose] !== void 0) opts.verbose = readBool(env[PIWI_ENV_KEYS.verbose]);
378
390
  return opts;
379
391
  }
380
392
  function applyOptionsToEnv(options) {
@@ -469,8 +481,8 @@ function readHub(value) {
469
481
  }
470
482
  function readChannel(value) {
471
483
  if (!isObject(value)) return null;
472
- const path36 = text(value.path, 4096);
473
- return path36 && typeof value.token === "string" && TOKEN.test(value.token) ? { path: path36, token: value.token } : null;
484
+ const path37 = text(value.path, 4096);
485
+ return path37 && typeof value.token === "string" && TOKEN.test(value.token) ? { path: path37, token: value.token } : null;
474
486
  }
475
487
  function parseEditorDiscovery(content) {
476
488
  let value;
@@ -549,8 +561,8 @@ function parseRecordChannel(value) {
549
561
  const hash = text2.lastIndexOf("#");
550
562
  if (hash <= 0) return null;
551
563
  const token = text2.slice(hash + 1);
552
- const path36 = text2.slice(0, hash);
553
- return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path36.includes("\0") ? { path: path36, token } : null;
564
+ const path37 = text2.slice(0, hash);
565
+ return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path37.includes("\0") ? { path: path37, token } : null;
554
566
  }
555
567
  function isObject2(value) {
556
568
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -912,10 +924,10 @@ var FrameAcks = class {
912
924
  const now = this.now();
913
925
  this.due = Math.min(Math.max(now, this.due + this.intervalMs), now + this.intervalMs);
914
926
  if (this.due <= now) return void 0;
915
- return new Promise((resolve11) => {
927
+ return new Promise((resolve12) => {
916
928
  const timer = setTimeout(() => this.release(), this.due - now);
917
929
  timer.unref?.();
918
- this.waiting = { timer, resolve: resolve11 };
930
+ this.waiting = { timer, resolve: resolve12 };
919
931
  });
920
932
  }
921
933
  /** Whether a frame waits for its acknowledgment. */
@@ -1116,10 +1128,15 @@ function readAct(value) {
1116
1128
 
1117
1129
  // src/internal/config/editors.ts
1118
1130
  var PIWI_EDITOR_ENV = "PIWI_EDITOR";
1131
+ var TEST_RUNNER_REPORTER_ENV = "PW_TEST_REPORTER";
1132
+ function startedByTestRunner(env) {
1133
+ const reporter = env[TEST_RUNNER_REPORTER_ENV]?.trim();
1134
+ return !!reporter && /[\\/]/.test(reporter);
1135
+ }
1119
1136
  function terminalRunSkip(gate) {
1120
1137
  const { env, argv } = gate;
1121
1138
  const editor = env[PIWI_EDITOR_ENV]?.trim().toLowerCase();
1122
- if (editor === "off") return `${PIWI_EDITOR_ENV}=off`;
1139
+ if (editor && ["off", "false", "0", "no"].includes(editor)) return `${PIWI_EDITOR_ENV}=${editor}`;
1123
1140
  if (env[RUN_CHANNEL_ENV] !== void 0) return `the run names its ${RUN_CHANNEL_ENV}`;
1124
1141
  if (env[PIWI_ORIGIN_ENV.kind]) return "a launcher started the run";
1125
1142
  if (detectCi(env)) return "running under CI";
@@ -1129,7 +1146,8 @@ function terminalRunSkip(gate) {
1129
1146
  return "Playwright's debugger or UI mode runs it";
1130
1147
  }
1131
1148
  if (argv.slice(2).includes("merge-reports")) return "it merges reports, running no test";
1132
- if (!gate.terminal && editor !== "on") return "its output goes to no terminal";
1149
+ const forced = !!editor && ["on", "true", "1", "yes"].includes(editor);
1150
+ if (!gate.terminal && !forced && !startedByTestRunner(env)) return "its output goes to no terminal";
1133
1151
  return null;
1134
1152
  }
1135
1153
  function editorsFolder(env = process.env) {
@@ -1445,7 +1463,7 @@ var HttpClient = class {
1445
1463
  }
1446
1464
  /** Unified request core: transport, headers, auth, response accumulation, timeout. */
1447
1465
  request(method, pathname, opts) {
1448
- return new Promise((resolve11, reject) => {
1466
+ return new Promise((resolve12, reject) => {
1449
1467
  if (this.closedReason !== null) {
1450
1468
  reject(new Error(this.closedReason));
1451
1469
  return;
@@ -1468,7 +1486,7 @@ var HttpClient = class {
1468
1486
  data += chunk;
1469
1487
  });
1470
1488
  res.on("end", () => {
1471
- resolve11({ status: res.statusCode ?? 0, text: data, headers: res.headers });
1489
+ resolve12({ status: res.statusCode ?? 0, text: data, headers: res.headers });
1472
1490
  });
1473
1491
  res.on("error", reject);
1474
1492
  res.on("close", () => {
@@ -1511,6 +1529,87 @@ var fs4 = __toESM(require("fs"));
1511
1529
  var path4 = __toESM(require("path"));
1512
1530
  var import_form_data2 = __toESM(require("form-data"));
1513
1531
 
1532
+ // ../core/src/status-classify.ts
1533
+ function mergeAnnotations(test, result) {
1534
+ const out = [];
1535
+ const seen = /* @__PURE__ */ new Set();
1536
+ for (const list of [test.annotations, result.annotations]) {
1537
+ for (const a of list ?? []) {
1538
+ const key = `${a.type}\0${a.description ?? ""}`;
1539
+ if (seen.has(key)) continue;
1540
+ seen.add(key);
1541
+ out.push(a.description === void 0 ? { type: a.type } : { type: a.type, description: a.description });
1542
+ }
1543
+ }
1544
+ return out;
1545
+ }
1546
+ function expectsFailure(annotations) {
1547
+ if (annotations.some((a) => a.type === "skip" || a.type === "fixme")) return false;
1548
+ return annotations.some((a) => a.type === "fail");
1549
+ }
1550
+ var EXPECTED_FAILURE_PASSED_MESSAGE = "Expected to fail, but passed.";
1551
+ function expectedFailureError(rawStatus, annotations) {
1552
+ return rawStatus === "passed" && expectsFailure(annotations) ? EXPECTED_FAILURE_PASSED_MESSAGE : null;
1553
+ }
1554
+ function isUnexpectedStatus(status) {
1555
+ return status === "failed" || status === "timedout" || status === "timedOut";
1556
+ }
1557
+ function isFlakyResult(lastStatus, earlierStatuses) {
1558
+ if (lastStatus !== "passed") return false;
1559
+ for (const status of earlierStatuses) if (isUnexpectedStatus(status)) return true;
1560
+ return false;
1561
+ }
1562
+ function classifyStatus(rawStatus, annotations) {
1563
+ if (rawStatus === "skipped") {
1564
+ const intentional = annotations.some((a) => a.type === "skip" || a.type === "fixme");
1565
+ return intentional ? "skipped" : "didnotrun";
1566
+ }
1567
+ if (expectsFailure(annotations)) {
1568
+ if (rawStatus === "failed") return "passed";
1569
+ if (rawStatus === "passed") return "failed";
1570
+ }
1571
+ return rawStatus;
1572
+ }
1573
+ function resolveUnrunReason(runStatus, opts) {
1574
+ if (runStatus === "timedout") return "global-timeout";
1575
+ if (opts.maxFailures > 0 && opts.failures >= opts.maxFailures) return "max-failures";
1576
+ return "interrupted";
1577
+ }
1578
+ function fileOf(location) {
1579
+ return location.replace(/:\d+:\d+$/, "");
1580
+ }
1581
+ function suiteKey(suitePath) {
1582
+ return (suitePath ?? []).join("");
1583
+ }
1584
+ function sharedPrefixDepth(a, b) {
1585
+ let depth = 0;
1586
+ while (depth < a.length && depth < b.length && a[depth] === b[depth]) depth++;
1587
+ return depth;
1588
+ }
1589
+ function linkBlockedTests(cases) {
1590
+ const blockers = cases.filter((c) => c.status === "failed" || c.status === "timedOut" || c.status === "timedout");
1591
+ if (blockers.length === 0) return;
1592
+ for (const c of cases) {
1593
+ if (c.didNotRunReason !== "previous-failure") continue;
1594
+ const file = fileOf(c.location);
1595
+ const inFile = blockers.filter((b) => fileOf(b.location) === file);
1596
+ if (inFile.length === 0) continue;
1597
+ const key = suiteKey(c.suitePath);
1598
+ let blocker = inFile.find((b) => suiteKey(b.suitePath) === key);
1599
+ if (!blocker) {
1600
+ let bestDepth = 0;
1601
+ for (const b of inFile) {
1602
+ const depth = sharedPrefixDepth(c.suitePath ?? [], b.suitePath ?? []);
1603
+ if (depth > bestDepth) {
1604
+ bestDepth = depth;
1605
+ blocker = b;
1606
+ }
1607
+ }
1608
+ }
1609
+ if (blocker) c.blockedBy = blocker.location;
1610
+ }
1611
+ }
1612
+
1514
1613
  // src/internal/submit/serializer.ts
1515
1614
  function resolveOverallStatus(result, counters) {
1516
1615
  const STATUS_MAP = {
@@ -1586,6 +1685,7 @@ function isFailingStatus(status) {
1586
1685
  }
1587
1686
  function computeDistinctRunCounts(testCases) {
1588
1687
  const final = /* @__PURE__ */ new Map();
1688
+ const attemptsOf = /* @__PURE__ */ new Map();
1589
1689
  for (const tc of testCases) {
1590
1690
  const key = tc.testId ?? [
1591
1691
  tc.title,
@@ -1596,6 +1696,9 @@ function computeDistinctRunCounts(testCases) {
1596
1696
  ].join("\0");
1597
1697
  const status = tc.status ?? "";
1598
1698
  const retries = tc.retries ?? 0;
1699
+ const attempts = attemptsOf.get(key);
1700
+ if (attempts) attempts.push({ status, retries });
1701
+ else attemptsOf.set(key, [{ status, retries }]);
1599
1702
  const prev = final.get(key);
1600
1703
  const decides = !prev || retries > prev.retries || retries === prev.retries && (isFailingStatus(status) || !isFailingStatus(prev.status));
1601
1704
  if (decides) final.set(key, { status, retries });
@@ -1606,11 +1709,15 @@ function computeDistinctRunCounts(testCases) {
1606
1709
  failedTests: 0,
1607
1710
  timedOutTests: 0,
1608
1711
  skippedTests: 0,
1609
- didNotRunTests: 0
1712
+ didNotRunTests: 0,
1713
+ flakyTests: 0
1610
1714
  };
1611
- for (const { status } of final.values()) {
1612
- if (status === "passed") counts.passedTests++;
1613
- else if (status === "failed") counts.failedTests++;
1715
+ for (const [key, { status, retries }] of final) {
1716
+ if (status === "passed") {
1717
+ counts.passedTests++;
1718
+ const earlier = (attemptsOf.get(key) ?? []).filter((a) => a.retries < retries).map((a) => a.status);
1719
+ if (isFlakyResult(status, earlier)) counts.flakyTests++;
1720
+ } else if (status === "failed") counts.failedTests++;
1614
1721
  else if (status === "timedOut" || status === "timedout") counts.timedOutTests++;
1615
1722
  else if (status === "skipped") counts.skippedTests++;
1616
1723
  else if (status === "didnotrun") counts.didNotRunTests++;
@@ -1686,12 +1793,12 @@ var Uploader = class {
1686
1793
  await this.appendReportsToForm(form, reportOptions.reports, reportOptions.uploadReport);
1687
1794
  await this.appendFilesToForm(form, payload.testCases, reportOptions.uploadTraces);
1688
1795
  const response = await this.httpClient.postFormData("/api/test-runs/upload", form, auth);
1689
- this.logger.info(`Successfully uploaded test results with files`);
1796
+ this.logger.debug(`Successfully uploaded test results with files`);
1690
1797
  if (response.runId) {
1691
- this.logger.info(`Test Run ID: ${response.runId}, Project ID: ${response.projectId}`);
1798
+ this.logger.debug(`Test Run ID: ${response.runId}, Project ID: ${response.projectId}`);
1692
1799
  }
1693
1800
  if (response.reports) {
1694
- for (const r of response.reports) this.logger.info(`${r.label}: ${r.path}`);
1801
+ for (const r of response.reports) this.logger.debug(`${r.label}: ${r.path}`);
1695
1802
  }
1696
1803
  return response;
1697
1804
  }
@@ -1716,9 +1823,9 @@ var Uploader = class {
1716
1823
  form.append("testCases", JSON.stringify([]));
1717
1824
  await this.appendReportsToForm(form, reportOptions.reports, reportOptions.uploadReport);
1718
1825
  const response = await this.httpClient.postFormData("/api/test-runs/upload", form, auth);
1719
- this.logger.info(`Successfully uploaded reports for streaming run #${runId}`);
1826
+ this.logger.debug(`Successfully uploaded reports for streaming run #${runId}`);
1720
1827
  if (response.reports) {
1721
- for (const r of response.reports) this.logger.info(`${r.label}: ${r.path}`);
1828
+ for (const r of response.reports) this.logger.debug(`${r.label}: ${r.path}`);
1722
1829
  }
1723
1830
  }
1724
1831
  /**
@@ -1839,7 +1946,7 @@ var Uploader = class {
1839
1946
  }
1840
1947
  }
1841
1948
  }
1842
- this.logger.info(`Found ${traceCount} trace files`);
1949
+ this.logger.debug(`Found ${traceCount} trace files`);
1843
1950
  }
1844
1951
  };
1845
1952
 
@@ -1959,7 +2066,7 @@ var CrashRecovery = class {
1959
2066
  try {
1960
2067
  fs6.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
1961
2068
  fs6.renameSync(temp, this.filePath);
1962
- this.logger.info("Saved recovery data for later upload");
2069
+ this.logger.debug("Saved recovery data for later upload");
1963
2070
  } catch (error) {
1964
2071
  fs6.rmSync(temp, { force: true });
1965
2072
  this.logger.error(`Failed to save recovery data: ${errorMessage(error)}`);
@@ -2342,8 +2449,8 @@ var FileHandler = class {
2342
2449
  }
2343
2450
  if (!lastPath) return null;
2344
2451
  const hash = crypto4.createHash("sha256");
2345
- await new Promise((resolve11, reject) => {
2346
- fs8.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve11).on("error", reject);
2452
+ await new Promise((resolve12, reject) => {
2453
+ fs8.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve12).on("error", reject);
2347
2454
  });
2348
2455
  return {
2349
2456
  tracePath: lastPath,
@@ -2819,10 +2926,10 @@ function createLimiter(maxConcurrent) {
2819
2926
  if (run) run();
2820
2927
  };
2821
2928
  return function limit(fn) {
2822
- return new Promise((resolve11, reject) => {
2929
+ return new Promise((resolve12, reject) => {
2823
2930
  const run = () => {
2824
2931
  active2++;
2825
- Promise.resolve().then(fn).then(resolve11, reject).finally(next);
2932
+ Promise.resolve().then(fn).then(resolve12, reject).finally(next);
2826
2933
  };
2827
2934
  if (active2 < limitValue) run();
2828
2935
  else queue.push(run);
@@ -2871,6 +2978,7 @@ var SERVER_MAX_EVENT_BATCH_BYTES = 10 * 1024 * 1024;
2871
2978
  var EVENT_ENVELOPE_BYTES = 1024;
2872
2979
  var MAX_EVENT_REQUEST_BYTES = 4 * 1024 * 1024;
2873
2980
  var MAX_UPLOAD_CONNECTION_FAILURES = 3;
2981
+ var MIN_HEARTBEAT_WAIT_MS = 250;
2874
2982
  var StreamManager = class {
2875
2983
  /**
2876
2984
  * @param httpClient HTTP client for server communication.
@@ -3066,7 +3174,7 @@ var StreamManager = class {
3066
3174
  this.streamBuffer.bindRun(response.runId);
3067
3175
  this.logger.info(`Streaming enabled. Run ID: ${response.runId}`);
3068
3176
  if (this.options.serverUrl) {
3069
- this.logger.info(`Watch live: ${runUrl(this.options.serverUrl, response.runId)}`);
3177
+ this.logger.info(`View live progress: ${runUrl(this.options.serverUrl, response.runId)}`);
3070
3178
  }
3071
3179
  this.lastActivityAt = Date.now();
3072
3180
  this.scheduleHeartbeat();
@@ -3233,12 +3341,12 @@ var StreamManager = class {
3233
3341
  return this.watched === false ? this.unwatchedHeartbeatInterval : this.heartbeatInterval;
3234
3342
  }
3235
3343
  // Schedule the next idle heartbeat. Self-rescheduling; cleared by stopHeartbeat.
3236
- scheduleHeartbeat() {
3344
+ scheduleHeartbeat(delay = this.heartbeatDelay()) {
3237
3345
  if (this.heartbeatStopped || this.heartbeatTimer) return;
3238
3346
  this.heartbeatTimer = setTimeout(() => {
3239
3347
  this.heartbeatTimer = null;
3240
3348
  void this.sendHeartbeat();
3241
- }, this.heartbeatDelay());
3349
+ }, delay);
3242
3350
  }
3243
3351
  // Ping the server only when the run has actually been idle. Real event traffic
3244
3352
  // already bumps the server's activity timestamp, so a recent flush (or pending
@@ -3247,7 +3355,7 @@ var StreamManager = class {
3247
3355
  if (this.heartbeatStopped || !this._enabled || !this._runId || !this._token) return;
3248
3356
  const idleFor = Date.now() - this.lastActivityAt;
3249
3357
  if (idleFor < this.heartbeatDelay() || this.pendingEvents.length > 0) {
3250
- this.scheduleHeartbeat();
3358
+ this.scheduleHeartbeat(Math.max(this.heartbeatDelay() - idleFor, MIN_HEARTBEAT_WAIT_MS));
3251
3359
  return;
3252
3360
  }
3253
3361
  try {
@@ -3320,7 +3428,7 @@ var StreamManager = class {
3320
3428
  this.logger.debugError(
3321
3429
  `${this.pendingEvents.length} events pending, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`
3322
3430
  );
3323
- await new Promise((resolve11) => setTimeout(resolve11, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
3431
+ await new Promise((resolve12) => setTimeout(resolve12, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
3324
3432
  }
3325
3433
  if (!this.pendingEvents.isEmpty) {
3326
3434
  const remaining = this.pendingEvents.takeAll();
@@ -3377,7 +3485,7 @@ var StreamManager = class {
3377
3485
  if (flush) await flush;
3378
3486
  const delays = [0, 1e3, 3e3];
3379
3487
  for (let attempt = 0; attempt < delays.length; attempt++) {
3380
- if (delays[attempt]) await new Promise((resolve11) => setTimeout(resolve11, delays[attempt]));
3488
+ if (delays[attempt]) await new Promise((resolve12) => setTimeout(resolve12, delays[attempt]));
3381
3489
  try {
3382
3490
  await this.limitLiveUpload(
3383
3491
  () => this.uploader.uploadCaseFiles(
@@ -4198,6 +4306,13 @@ var SECRET_AUTOCOMPLETE = [
4198
4306
  "cc-number",
4199
4307
  "cc-csc"
4200
4308
  ];
4309
+ var SECRET_FIELD_SELECTOR = [
4310
+ 'input[type="password" i]',
4311
+ ...SECRET_AUTOCOMPLETE.flatMap((token) => [
4312
+ `input[autocomplete~="${token}" i]`,
4313
+ `textarea[autocomplete~="${token}" i]`
4314
+ ])
4315
+ ].join(", ");
4201
4316
  var SECRET_WORDS = /* @__PURE__ */ new Set([
4202
4317
  "pass",
4203
4318
  "password",
@@ -4234,7 +4349,7 @@ var SECRET_WORDS = /* @__PURE__ */ new Set([
4234
4349
  ]);
4235
4350
  var SECRET_NAMES = /* @__PURE__ */ new Set(["code", "pin"]);
4236
4351
  function nameWords(name) {
4237
- return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
4352
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])(?=[A-Z][a-z])/g, "$1 ").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
4238
4353
  }
4239
4354
  function isSecretName(name) {
4240
4355
  let secret = namesRead.get(name);
@@ -4358,9 +4473,9 @@ function hideParams(params, count) {
4358
4473
  return isSecretName(decodeSafe(name)) || isSecretValue(decoded) ? `${name}=${hide()}` : part;
4359
4474
  }).join("");
4360
4475
  }
4361
- function hidePath(path36, count) {
4476
+ function hidePath(path37, count) {
4362
4477
  const hide = hider(count);
4363
- return path36.split("/").map((segment) => {
4478
+ return path37.split("/").map((segment) => {
4364
4479
  const [own = "", ...params] = segment.split(";");
4365
4480
  const decoded = decodeSafe(own);
4366
4481
  const kept = decoded !== HIDDEN_VALUE && isSecretValue(decoded) ? hide() : own;
@@ -4370,14 +4485,14 @@ function hidePath(path36, count) {
4370
4485
  function hideSecretsInUrl(url, count = { n: 0 }) {
4371
4486
  const parts = URL_PARTS.exec(url);
4372
4487
  if (!parts) return url;
4373
- const [, scheme = "", authority, path36 = "", query = "", fragment = ""] = parts;
4488
+ const [, scheme = "", authority, path37 = "", query = "", fragment = ""] = parts;
4374
4489
  let out = "";
4375
4490
  if (authority !== void 0) {
4376
4491
  const at = authority.lastIndexOf("@");
4377
4492
  const colon = at >= 0 ? authority.slice(0, at).indexOf(":") : -1;
4378
4493
  out += colon >= 0 && colon < at - 1 ? `${scheme}//${authority.slice(0, colon + 1)}${hider(count)()}${authority.slice(at)}` : `${scheme}//${authority}`;
4379
4494
  }
4380
- out += hidePath(path36, count);
4495
+ out += hidePath(path37, count);
4381
4496
  if (query) out += `?${hideParams(query.slice(1), count)}`;
4382
4497
  if (fragment) {
4383
4498
  const body = fragment.slice(1);
@@ -4408,10 +4523,10 @@ function hideInText(text2, count) {
4408
4523
  if (text2.length < 4 || !/[A-Za-z0-9]/.test(text2)) return text2;
4409
4524
  const hide = hider(count);
4410
4525
  let out = hidePem(text2, hide);
4411
- out = out.replace(QUERY_PAIR, (all, sep11, name, value) => {
4526
+ out = out.replace(QUERY_PAIR, (all, sep12, name, value) => {
4412
4527
  const decoded = decodeSafe(value);
4413
4528
  if (!value || decoded === HIDDEN_VALUE) return all;
4414
- return isSecretName(name) || isSecretValue(decoded) ? `${sep11}${name}=${hide()}` : all;
4529
+ return isSecretName(name) || isSecretValue(decoded) ? `${sep12}${name}=${hide()}` : all;
4415
4530
  });
4416
4531
  out = out.replace(URL_PASSWORD, (_all, before, _password, at) => `${before}${hide()}${at}`);
4417
4532
  out = out.replace(SET_COOKIE, (_all, before) => `${before}${hide()}`);
@@ -4442,9 +4557,149 @@ function hideSecretsInText(text2, count = { n: 0 }) {
4442
4557
  }
4443
4558
  return out + hideInText(text2.slice(last), count);
4444
4559
  }
4560
+ var TYPED_TITLE = /^(?:Fill|Type|Insert) "/;
4561
+ function hideTypedText(title) {
4562
+ const typing = TYPED_TITLE.exec(title);
4563
+ if (!typing) return title;
4564
+ const start = typing[0].length;
4565
+ const end = title.endsWith('"') ? title.length - 1 : title.lastIndexOf('" ');
4566
+ return end >= start ? `${title.slice(0, start)}${HIDDEN_VALUE}${title.slice(end)}` : `${typing[0]}${HIDDEN_VALUE}"`;
4567
+ }
4568
+ var SECRET_FIELD_WORDS = /* @__PURE__ */ new Set([
4569
+ "passe",
4570
+ "mdp",
4571
+ "passwort",
4572
+ "kennwort",
4573
+ "contrasena",
4574
+ "clave",
4575
+ "senha",
4576
+ "cvv",
4577
+ "cvc",
4578
+ "csc",
4579
+ "pin",
4580
+ "pw",
4581
+ "totp",
4582
+ "mfa",
4583
+ "2fa"
4584
+ ]);
4585
+ var SECRET_STEMS = ["password", "passwd", "passwort", "kennwort", "contrasena", "senha", "motdepasse"];
4586
+ var SECRET_CODE_KINDS = /* @__PURE__ */ new Set([
4587
+ "verification",
4588
+ "security",
4589
+ "confirmation",
4590
+ "auth",
4591
+ "authentication",
4592
+ "access",
4593
+ "time",
4594
+ "otp",
4595
+ "sms",
4596
+ "login",
4597
+ "mfa",
4598
+ "2fa"
4599
+ ]);
4600
+ function isSecretLocator(locator) {
4601
+ const plain = locator.normalize("NFD").replace(/[̀-ͯ]/g, "");
4602
+ if (/type\s*=\s*["']?password\b/i.test(plain)) return true;
4603
+ for (const quoted of plain.matchAll(/(['"])((?:(?!\1)[^\\]|\\.)*)\1/g)) if (isSecretName(quoted[2])) return true;
4604
+ const words = nameWords(plain);
4605
+ return words.some(
4606
+ (word) => SECRET_WORDS.has(word) || SECRET_FIELD_WORDS.has(word) || SECRET_STEMS.some((stem) => word.includes(stem))
4607
+ ) || words.includes("code") && words.some((word) => SECRET_CODE_KINDS.has(word));
4608
+ }
4609
+ var TYPED_CALL = (
4610
+ // eslint-disable-next-line no-control-regex -- matches the ESC byte of a color code
4611
+ /^((?:\u001b\[[\d;]*m)*[ \t]*(?:(?:-|\d+ ×)[ \t]+)?(?:\w+\.)?)(fill|type|insertText)\("/gm
4612
+ );
4613
+ var TYPED_END = /"\)(?=(?:\u001b\[[\d;]*m|[ \t])*(?:\r?\n|$))/g;
4614
+ function hideTypedTextInLog(text2) {
4615
+ let out = "";
4616
+ let from = 0;
4617
+ TYPED_CALL.lastIndex = 0;
4618
+ for (let call = TYPED_CALL.exec(text2); call; call = TYPED_CALL.exec(text2)) {
4619
+ out += `${text2.slice(from, call.index)}${call[1]}${call[2]}("${HIDDEN_VALUE}")`;
4620
+ TYPED_END.lastIndex = call.index + call[0].length;
4621
+ const end = TYPED_END.exec(text2);
4622
+ if (!end) return out;
4623
+ from = end.index + 2;
4624
+ TYPED_CALL.lastIndex = from;
4625
+ }
4626
+ return out + text2.slice(from);
4627
+ }
4628
+ var READ_LOCATOR = /(?:waiting for|Locator:)[ \t]+(.*)$/gm;
4629
+ var RESOLVED_PASSWORD = /resolved to <input\b[^>\n]*\btype="password"/i;
4630
+ var READ_VALUE = (
4631
+ // eslint-disable-next-line no-control-regex -- matches the ESC byte of a color code
4632
+ /(unexpected value |(?:Expected|Received)(?: string)?:[ \t]*(?:\u001b\[[\d;]*m)*(?:not[ \t]+)?)"(?:.*"|.*)/g
4633
+ );
4634
+ var RESOLVED_ELEMENT = /resolved to <[^>\n]*/g;
4635
+ var VALUE_ATTRIBUTE = /\bvalue="[^"\n]*"/g;
4636
+ var COLOR = /\u001b\[[\d;]*m/g;
4637
+ function namesSecretField(text2) {
4638
+ if (RESOLVED_PASSWORD.test(text2)) return true;
4639
+ for (const named of text2.matchAll(READ_LOCATOR))
4640
+ if (isSecretLocator(named[1].replace(COLOR, "").trim())) return true;
4641
+ return false;
4642
+ }
4643
+ function hideReadValues(text2) {
4644
+ return text2.replace(READ_VALUE, (_, before) => `${before}"${HIDDEN_VALUE}"`);
4645
+ }
4646
+ function hideResolvedValues(text2, secretField) {
4647
+ if (!text2.includes("resolved to <")) return text2;
4648
+ return text2.replace(
4649
+ RESOLVED_ELEMENT,
4650
+ (element) => secretField || /\btype="password"/i.test(element) ? element.replace(VALUE_ATTRIBUTE, `value="${HIDDEN_VALUE}"`) : element
4651
+ );
4652
+ }
4653
+ var LOGGED_HEADER = (
4654
+ // eslint-disable-next-line no-control-regex -- matches the ESC byte of a color code
4655
+ /^(?:\u001b\[[\d;]*m)*(?:[ \t]+(?:(?:-|\d+ ×)[ \t]+)?|(?:-|\d+ ×)[ \t]+)([A-Za-z][\w-]*): /
4656
+ );
4657
+ function colorsAtEnd(line) {
4658
+ let end = line.length;
4659
+ while (end > 0 && line[end - 1] === "m") {
4660
+ const start = line.lastIndexOf("\x1B[", end - 1);
4661
+ if (start < 0 || !/^[\d;]*$/.test(line.slice(start + 2, end - 1))) break;
4662
+ end = start;
4663
+ }
4664
+ return end;
4665
+ }
4666
+ function hideSecretHeaders(text2) {
4667
+ if (!text2.includes(": ")) return text2;
4668
+ return text2.split("\n").map((line) => {
4669
+ const header = LOGGED_HEADER.exec(line);
4670
+ if (!header || !isSecretName(header[1])) return line;
4671
+ const rest = line.slice(header[0].length);
4672
+ const end = colorsAtEnd(rest);
4673
+ return end === 0 ? line : `${header[0]}${HIDDEN_VALUE}${rest.slice(end)}`;
4674
+ }).join("\n");
4675
+ }
4676
+ function hideSecretsInLog(text2, secretField = false) {
4677
+ const secret = secretField || namesSecretField(text2);
4678
+ let hidden2 = hideResolvedValues(hideTypedTextInLog(text2), secret);
4679
+ if (secret) hidden2 = hideReadValues(hidden2);
4680
+ return hideSecretHeaders(hidden2);
4681
+ }
4682
+ var CALL_LOG = "\nCall log:";
4683
+ var CALL_LOG_END = /\n(?:---\n|[ \t]+at )/g;
4684
+ function hideSecretsInError(text2) {
4685
+ const secretField = namesSecretField(text2);
4686
+ let out = "";
4687
+ let from = 0;
4688
+ for (; ; ) {
4689
+ const log = text2.indexOf(CALL_LOG, from);
4690
+ const message = text2.slice(from, log === -1 ? text2.length : log);
4691
+ out += secretField ? hideReadValues(message) : message;
4692
+ if (log === -1) return out;
4693
+ const start = log + CALL_LOG.length;
4694
+ CALL_LOG_END.lastIndex = start;
4695
+ const end = CALL_LOG_END.exec(text2)?.index ?? text2.length;
4696
+ out += CALL_LOG + hideSecretsInLog(text2.slice(start, end), secretField);
4697
+ from = end;
4698
+ }
4699
+ }
4445
4700
 
4446
4701
  // ../core/src/locator-break.ts
4447
- var filePath2 = (path36) => path36.replace(/\\/g, "/").replace(/^\.\//, "");
4702
+ var filePath2 = (path37) => path37.replace(/\\/g, "/").replace(/^\.\//, "");
4448
4703
  function sameFilePath(a, b) {
4449
4704
  const x = filePath2(a);
4450
4705
  const y = filePath2(b);
@@ -4713,20 +4968,47 @@ function markRecoveredSteps(steps, errors) {
4713
4968
  function firstErrorLine(message) {
4714
4969
  return stripAnsi(typeof message === "string" ? message : "").split("\n")[0].trim();
4715
4970
  }
4971
+ var TYPED_PARAMS = ["value", "text"];
4972
+ var BARE_TYPED_TITLE = /^(?:Fill|Type|Insert)$/;
4973
+ var OWN_CATEGORIES = /* @__PURE__ */ new Set(["test.step", "hook", "fixture"]);
4974
+ function withoutTypedText(title, params, subtitle, category) {
4975
+ if (category && OWN_CATEGORIES.has(category)) return { title, params };
4976
+ const hidden2 = hideTypedText(title);
4977
+ const bare = BARE_TYPED_TITLE.test(title.trim());
4978
+ if (hidden2 === title && !bare) return { title, params };
4979
+ const named = bare ? "" : hidden2.slice(hidden2.indexOf(`${HIDDEN_VALUE}"`) + HIDDEN_VALUE.length + 1).trim();
4980
+ const locators = [params?.locator, subtitle, named].filter(
4981
+ (locator) => typeof locator === "string" && locator.length > 0
4982
+ );
4983
+ if (locators.length && !locators.some(isSecretLocator)) return { title, params };
4984
+ if (!params) return { title: hidden2, params };
4985
+ const out = { ...params };
4986
+ for (const key of TYPED_PARAMS) if (typeof out[key] === "string") out[key] = HIDDEN_VALUE;
4987
+ return { title: hidden2, params: out };
4988
+ }
4989
+ function hideTypedStepTitle(title, category) {
4990
+ return category && OWN_CATEGORIES.has(category) ? title : hideTypedText(title);
4991
+ }
4716
4992
  function flattenSteps(steps, depth = 0) {
4717
4993
  const result = [];
4718
4994
  for (const step of steps) {
4995
+ const named = [step.subtitle, step.params?.locator].filter((v) => typeof v === "string" && v).join(" ");
4996
+ const typed = withoutTypedText(
4997
+ typeof step.title === "string" ? step.title : "",
4998
+ normalizeStepParams(step.params),
4999
+ named || null,
5000
+ step.category
5001
+ );
4719
5002
  const flat = {
4720
- title: step.title,
5003
+ title: typed.title,
4721
5004
  duration: step.duration,
4722
5005
  category: categorizeStep(step.title, step.category, step.params),
4723
5006
  depth
4724
5007
  };
4725
5008
  if (typeof step.subtitle === "string" && step.subtitle.length > 0) flat.subtitle = maskTokenLike(step.subtitle);
4726
- const params = normalizeStepParams(step.params);
4727
- if (params) flat.params = params;
5009
+ if (typed.params) flat.params = typed.params;
4728
5010
  if (step.error?.message) {
4729
- flat.error = { message: step.error.message };
5011
+ flat.error = { message: hideSecretsInError(step.error.message) };
4730
5012
  const thrownAt = locationText(step.error.location);
4731
5013
  if (thrownAt) flat.error.location = thrownAt;
4732
5014
  flat.failed = true;
@@ -4989,7 +5271,7 @@ function collectSourceFrames(errorText2, testFile2, declLine, opts = {}) {
4989
5271
  if (picked.length >= maxFrames) break;
4990
5272
  let abs;
4991
5273
  try {
4992
- abs = path12.resolve(frame.file);
5274
+ abs = path12.resolve(projectRoot, frame.file);
4993
5275
  } catch {
4994
5276
  continue;
4995
5277
  }
@@ -5000,7 +5282,7 @@ function collectSourceFrames(errorText2, testFile2, declLine, opts = {}) {
5000
5282
  }
5001
5283
  const absTest = path12.resolve(testFile2);
5002
5284
  if (!picked.some((p) => p.absFile === absTest)) {
5003
- add2(absTest, extractFailingLine(errorText2, testFile2, declLine));
5285
+ add2(absTest, extractFailingLine(errorText2, testFile2, declLine, projectRoot));
5004
5286
  }
5005
5287
  const frames = [];
5006
5288
  for (const p of picked.slice(0, maxFrames)) {
@@ -5011,12 +5293,12 @@ function collectSourceFrames(errorText2, testFile2, declLine, opts = {}) {
5011
5293
  }
5012
5294
  return frames;
5013
5295
  }
5014
- function extractFailingLine(errorText2, testFile2, declarationLine) {
5296
+ function extractFailingLine(errorText2, testFile2, declarationLine, base = process.cwd()) {
5015
5297
  if (!errorText2) return declarationLine;
5016
5298
  const expectedFile = path12.resolve(testFile2);
5017
5299
  for (const frame of stackFrames(errorText2)) {
5018
5300
  try {
5019
- if (path12.resolve(frame.file) === expectedFile && !isNaN(frame.line)) return frame.line;
5301
+ if (path12.resolve(base, frame.file) === expectedFile && !isNaN(frame.line)) return frame.line;
5020
5302
  } catch {
5021
5303
  }
5022
5304
  }
@@ -5030,40 +5312,87 @@ function workerIndexOf(result) {
5030
5312
  }
5031
5313
 
5032
5314
  // src/internal/support/cli-filters.ts
5033
- var PW_VALUE_FLAGS = /* @__PURE__ */ new Set([
5315
+ var REQUIRED_VALUE_FLAGS = /* @__PURE__ */ new Set([
5316
+ "--add-reporter",
5317
+ "--browser",
5034
5318
  "-c",
5035
5319
  "--config",
5036
- "-j",
5037
- "--workers",
5320
+ "--global-timeout",
5038
5321
  "-g",
5039
5322
  "--grep",
5323
+ "-G",
5040
5324
  "--grep-invert",
5041
- "--global-timeout",
5325
+ "--last-failed-file",
5042
5326
  "--max-failures",
5043
5327
  "--output",
5044
5328
  "-p",
5045
- "--project",
5046
5329
  "--repeat-each",
5047
5330
  "--reporter",
5048
5331
  "--retries",
5332
+ "--run-agents",
5049
5333
  "--shard",
5334
+ "--test-list",
5335
+ "--test-list-invert",
5050
5336
  "--timeout",
5051
- "--trace"
5337
+ "--trace",
5338
+ "--tsconfig",
5339
+ "--ui-host",
5340
+ "--ui-port",
5341
+ "--update-source-method",
5342
+ "-j",
5343
+ "--workers"
5052
5344
  ]);
5053
- function detectCliFileFilters(argv = process.argv) {
5345
+ var OPTIONAL_VALUE_FLAGS = /* @__PURE__ */ new Set(["--debug", "--only-changed", "--shuffle", "-u", "--update-snapshots"]);
5346
+ var VARIADIC_FLAGS = /* @__PURE__ */ new Set(["--project"]);
5347
+ var NARROWING_FLAGS = ["--last-failed", "--only-changed", "--test-list", "--test-list-invert"];
5348
+ function parseTestArguments(argv) {
5054
5349
  const args = argv.slice(2);
5350
+ const files = [];
5351
+ const options = /* @__PURE__ */ new Map();
5352
+ if (args.find((arg) => !arg.startsWith("-")) === "merge-reports") return { files, options };
5055
5353
  const testIdx = args.indexOf("test");
5056
5354
  const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
5057
- const files = [];
5058
5355
  for (let i = 0; i < rest.length; i++) {
5059
5356
  const tok = rest[i];
5060
- if (tok.startsWith("-")) {
5061
- if (!tok.includes("=") && PW_VALUE_FLAGS.has(tok)) i++;
5357
+ if (!tok.startsWith("-")) {
5358
+ files.push(tok);
5062
5359
  continue;
5063
5360
  }
5064
- files.push(tok);
5361
+ const eq = tok.indexOf("=");
5362
+ if (eq > 0) {
5363
+ options.set(tok.slice(0, eq), tok.slice(eq + 1));
5364
+ continue;
5365
+ }
5366
+ const next = rest[i + 1];
5367
+ if (REQUIRED_VALUE_FLAGS.has(tok) && next !== void 0) {
5368
+ options.set(tok, next);
5369
+ i++;
5370
+ } else if (OPTIONAL_VALUE_FLAGS.has(tok) && next !== void 0 && !next.startsWith("-")) {
5371
+ options.set(tok, next);
5372
+ i++;
5373
+ } else if (VARIADIC_FLAGS.has(tok)) {
5374
+ const values = [];
5375
+ while (rest[i + 1] !== void 0 && !rest[i + 1].startsWith("-")) values.push(rest[++i]);
5376
+ options.set(tok, values.join(","));
5377
+ } else {
5378
+ options.set(tok, true);
5379
+ }
5065
5380
  }
5066
- return files;
5381
+ return { files, options };
5382
+ }
5383
+ function detectCliFileFilters(argv = process.argv) {
5384
+ return parseTestArguments(argv).files;
5385
+ }
5386
+ function detectCliNarrowingFlags(argv = process.argv) {
5387
+ const { options } = parseTestArguments(argv);
5388
+ return [...options.keys()].filter((flag) => NARROWING_FLAGS.includes(flag)).map((flag) => flag.slice(2));
5389
+ }
5390
+ function detectCliGrep(argv = process.argv) {
5391
+ const { options } = parseTestArguments(argv);
5392
+ const valueOf = (...names) => names.map((name) => options.get(name)).find((v) => typeof v === "string" && v.length > 0);
5393
+ const grep = valueOf("--grep", "-g");
5394
+ const grepInvert = valueOf("--grep-invert", "-G");
5395
+ return { ...grep ? { grep } : {}, ...grepInvert ? { grepInvert } : {} };
5067
5396
  }
5068
5397
 
5069
5398
  // src/internal/support/selection-env.ts
@@ -5117,6 +5446,8 @@ var LIVE_FRAME_BYTE_RATE = 256 * 1024;
5117
5446
  var MAX_LIVE_FRAME = 192 * 1024;
5118
5447
  var MAX_LIVE_FRAME_BASE64 = 4 * Math.ceil(MAX_LIVE_FRAME / 3);
5119
5448
  var MAX_LIVE_MESSAGE = MAX_LIVE_FRAME_BASE64 + 8 * 1024;
5449
+ var MAX_LIVE_CALL_LOG_LINES = 20;
5450
+ var LIVE_CALL_LOGS_PER_SECOND = 4;
5120
5451
  var KEY = /^pdl_[A-Za-z0-9_-]{43}$/;
5121
5452
  function isLiveViewKey(value) {
5122
5453
  return typeof value === "string" && KEY.test(value);
@@ -5180,12 +5511,68 @@ function liveShareRefusal(status) {
5180
5511
  return "Sharing stopped: the instance did not answer. Share the run again to try once more.";
5181
5512
  }
5182
5513
  }
5514
+ var MAX_LIVE_MASKS = 20;
5515
+ var MAX_LIVE_MASK_LENGTH = 200;
5516
+ var FRAME_CALLS = /* @__PURE__ */ new Set(["frameLocator", "contentFrame", "owner"]);
5517
+ var NARROWING_CALLS = /* @__PURE__ */ new Set(["filter", "and", "or", "visible", "first", "last", "nth"]);
5518
+ function liveMaskProblem(locator) {
5519
+ if (!locator.trim()) return "A masked element needs a locator.";
5520
+ if (locator.length > MAX_LIVE_MASK_LENGTH) return `A locator is ${MAX_LIVE_MASK_LENGTH} characters at most.`;
5521
+ const source = locator.trim();
5522
+ const chain = tryParseLocatorChain(source);
5523
+ if (!chain?.calls.length || NARROWING_CALLS.has(chain.calls[0].method)) {
5524
+ return `\`${source}\` is not a Playwright locator, such as getByLabel('IBAN').`;
5525
+ }
5526
+ if (chain.calls.some((call) => FRAME_CALLS.has(call.method))) {
5527
+ return `\`${source}\` enters a frame: masks are looked for in every frame of the page, so name the element alone, such as getByLabel('Card number'), or the iframe, such as locator('iframe#card').`;
5528
+ }
5529
+ return null;
5530
+ }
5531
+ function parseLiveMasks(value) {
5532
+ if (!Array.isArray(value) || value.length > MAX_LIVE_MASKS) return null;
5533
+ const locators = [];
5534
+ for (const locator of value) {
5535
+ if (typeof locator !== "string" || liveMaskProblem(locator)) return null;
5536
+ if (!locators.includes(locator.trim())) locators.push(locator.trim());
5537
+ }
5538
+ return locators;
5539
+ }
5183
5540
  function isObject5(value) {
5184
5541
  return typeof value === "object" && value !== null && !Array.isArray(value);
5185
5542
  }
5186
5543
  function isInt2(value, min, max) {
5187
5544
  return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
5188
5545
  }
5546
+ var LONGEST_REPEAT = 12;
5547
+ function sameLines(lines, a, b, size) {
5548
+ for (let k = 0; k < size; k++) if (lines[a + k] !== lines[b + k]) return false;
5549
+ return true;
5550
+ }
5551
+ function compressCallLog(lines) {
5552
+ const out = [];
5553
+ let i = 0;
5554
+ while (i < lines.length) {
5555
+ let size = 1;
5556
+ let count = 1;
5557
+ for (let length = 1; length <= LONGEST_REPEAT && i + length * 2 <= lines.length; length++) {
5558
+ let times = 1;
5559
+ while (i + length * (times + 1) <= lines.length && sameLines(lines, i, i + length * times, length)) times++;
5560
+ if (times > 1 && times * length > count * size) {
5561
+ size = length;
5562
+ count = times;
5563
+ }
5564
+ }
5565
+ for (let k = 0; k < size; k++) {
5566
+ const line = lines[i + k];
5567
+ const indent = /^\s*/.exec(line)[0];
5568
+ if (count === 1) out.push(`${indent}- ${line.trim()}`);
5569
+ else if (k === 0) out.push(`${indent}${count} \xD7 ${line.trim()}`);
5570
+ else out.push(`${indent}${" ".repeat(`${count} \xD7 `.length - 2)}- ${line.trim()}`);
5571
+ }
5572
+ i += size * count;
5573
+ }
5574
+ return out;
5575
+ }
5189
5576
  function parseInstanceToWorker(data) {
5190
5577
  let value;
5191
5578
  try {
@@ -5193,10 +5580,24 @@ function parseInstanceToWorker(data) {
5193
5580
  } catch {
5194
5581
  return null;
5195
5582
  }
5196
- if (!isObject5(value) || value.type !== "viewers" || !isInt2(value.count, 0, 1e6)) return null;
5583
+ if (!isObject5(value)) return null;
5584
+ if (value.type === "masks") return { type: "masks", locators: parseLiveMasks(value.locators) };
5585
+ if (value.type !== "viewers" || !isInt2(value.count, 0, 1e6)) return null;
5197
5586
  return { type: "viewers", count: value.count };
5198
5587
  }
5199
5588
 
5589
+ // src/internal/streaming/live-view-note.ts
5590
+ function ciLiveViewNote(why) {
5591
+ switch (why) {
5592
+ case "off":
5593
+ return "Live view off: nobody can watch this run's tests on the dashboard while it runs. A Project admin turns it on in Project \u2192 Settings \u2192 Live view, for the runs that start after.";
5594
+ case "unregistered":
5595
+ return "Live view unavailable: the reporter's global setup did not register this run as it started (wrapConfig adds it), so nobody can watch its tests on the dashboard while it runs.";
5596
+ case "streaming-off":
5597
+ return "Live view unavailable: this run does not stream its results (streaming: false), so nobody can watch its tests on the dashboard while it runs.";
5598
+ }
5599
+ }
5600
+
5200
5601
  // src/public/global-setup.ts
5201
5602
  var path16 = __toESM(require("path"));
5202
5603
 
@@ -5393,7 +5794,7 @@ async function checkBaseUrl(target, options = {}) {
5393
5794
  } else {
5394
5795
  reason = outcome.error;
5395
5796
  }
5396
- if (attempt < attempts) await new Promise((resolve11) => setTimeout(resolve11, delayMs));
5797
+ if (attempt < attempts) await new Promise((resolve12) => setTimeout(resolve12, delayMs));
5397
5798
  }
5398
5799
  return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
5399
5800
  }
@@ -5431,7 +5832,7 @@ var path14 = __toESM(require("path"));
5431
5832
  var http2 = __toESM(require("http"));
5432
5833
  var https2 = __toESM(require("https"));
5433
5834
  function backendRequest(url, init) {
5434
- return new Promise((resolve11, reject) => {
5835
+ return new Promise((resolve12, reject) => {
5435
5836
  const target = new URL(url);
5436
5837
  const headers = { ...init.headers };
5437
5838
  if (init.body !== void 0) headers["Content-Length"] = Buffer.byteLength(init.body);
@@ -5446,7 +5847,7 @@ function backendRequest(url, init) {
5446
5847
  });
5447
5848
  res.on(
5448
5849
  "end",
5449
- () => resolve11({ status: res.statusCode ?? 0, headers: res.headers, text: Buffer.concat(chunks).toString("utf8") })
5850
+ () => resolve12({ status: res.statusCode ?? 0, headers: res.headers, text: Buffer.concat(chunks).toString("utf8") })
5450
5851
  );
5451
5852
  res.on("error", reject);
5452
5853
  res.on("close", () => {
@@ -5520,8 +5921,8 @@ function backendManifestFetch(insecure) {
5520
5921
  function instrumentationManifestUrls(baseUrl) {
5521
5922
  const base = new URL(baseUrl);
5522
5923
  const root = new URL(INSTRUMENTATION_MANIFEST_PATH, base).toString();
5523
- const path36 = base.pathname.replace(/\/+$/, "");
5524
- return path36 ? [new URL(`${path36}${INSTRUMENTATION_MANIFEST_PATH}`, base).toString(), root] : [root];
5924
+ const path37 = base.pathname.replace(/\/+$/, "");
5925
+ return path37 ? [new URL(`${path37}${INSTRUMENTATION_MANIFEST_PATH}`, base).toString(), root] : [root];
5525
5926
  }
5526
5927
  async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
5527
5928
  try {
@@ -5659,13 +6060,13 @@ function normalizeFunctionRanges(ranges) {
5659
6060
  }
5660
6061
  function normalizeCoveragePath(raw) {
5661
6062
  if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
5662
- const path36 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
5663
- if (!path36 || path36.startsWith("../") || path36.startsWith("/") || /^[A-Za-z]:\//.test(path36)) return null;
5664
- if (/(^|\/)node_modules\//.test(path36)) return null;
5665
- return path36;
6063
+ const path37 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
6064
+ if (!path37 || path37.startsWith("../") || path37.startsWith("/") || /^[A-Za-z]:\//.test(path37)) return null;
6065
+ if (/(^|\/)node_modules\//.test(path37)) return null;
6066
+ return path37;
5666
6067
  }
5667
- function fileEntry(path36, runtime, blob, functions = [], lines = []) {
5668
- const file = { path: path36, runtime };
6068
+ function fileEntry(path37, runtime, blob, functions = [], lines = []) {
6069
+ const file = { path: path37, runtime };
5669
6070
  if (blob && (functions.length > 0 || lines.length > 0)) file.blob = blob;
5670
6071
  if (functions.length > 0) file.functions = functions;
5671
6072
  if (lines.length > 0) file.lines = lines;
@@ -5684,11 +6085,11 @@ function sanitizeCoverage(raw) {
5684
6085
  const files = [];
5685
6086
  for (const item of raw.files) {
5686
6087
  if (!isRecord(item) || !isRuntime(item.runtime)) continue;
5687
- const path36 = normalizeCoveragePath(item.path);
5688
- if (path36 === null) continue;
6088
+ const path37 = normalizeCoveragePath(item.path);
6089
+ if (path37 === null) continue;
5689
6090
  const blob = typeof item.blob === "string" && BLOB_ID.test(item.blob) ? item.blob : void 0;
5690
6091
  files.push(
5691
- fileEntry(path36, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
6092
+ fileEntry(path37, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
5692
6093
  );
5693
6094
  }
5694
6095
  const merged = mergeCoverage(captureOf(files, raw.incomplete === true));
@@ -5967,13 +6368,13 @@ var ServerCoverageCollector = class {
5967
6368
  if (parts.length === 0 || this.closer.signal.aborted) {
5968
6369
  return Promise.resolve({ files: [], incomplete: truncated || parts.length > 0 });
5969
6370
  }
5970
- return new Promise((resolve11) => {
6371
+ return new Promise((resolve12) => {
5971
6372
  const entry = {
5972
6373
  pending: parts.length,
5973
6374
  captures: [],
5974
6375
  checksums: /* @__PURE__ */ new Map(),
5975
6376
  incomplete: truncated,
5976
- resolve: resolve11
6377
+ resolve: resolve12
5977
6378
  };
5978
6379
  for (const [origin, ids] of parts) this.enqueue(origin, { entry, ids });
5979
6380
  });
@@ -6458,8 +6859,8 @@ function parseRouteKey(routeKey2) {
6458
6859
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
6459
6860
  }
6460
6861
  function patternToRegex(pattern) {
6461
- const path36 = pattern.split("?")[0] ?? pattern;
6462
- const escaped = path36.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
6862
+ const path37 = pattern.split("?")[0] ?? pattern;
6863
+ const escaped = path37.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
6463
6864
  return new RegExp(`^${escaped}$`);
6464
6865
  }
6465
6866
  function suitePathEqual(a, b) {
@@ -6482,13 +6883,13 @@ function matchProbeItem(plan, test) {
6482
6883
  function requestMatchesRoute(method, url, routeKey2) {
6483
6884
  const { method: rm, pattern } = parseRouteKey(routeKey2);
6484
6885
  if (rm && rm !== method.toUpperCase()) return false;
6485
- let path36;
6886
+ let path37;
6486
6887
  try {
6487
- path36 = new URL(url).pathname;
6888
+ path37 = new URL(url).pathname;
6488
6889
  } catch {
6489
- path36 = url.split("?")[0] ?? url;
6890
+ path37 = url.split("?")[0] ?? url;
6490
6891
  }
6491
- return patternToRegex(pattern).test(path36);
6892
+ return patternToRegex(pattern).test(path37);
6492
6893
  }
6493
6894
  function createProbeState() {
6494
6895
  return { navigated: false, matches: 0 };
@@ -6510,10 +6911,10 @@ function isProbeMode() {
6510
6911
  function loadPlan() {
6511
6912
  if (planCache !== void 0) return planCache;
6512
6913
  planCache = null;
6513
- const path36 = process.env[PIWI_PROBE_ENV.plan];
6514
- if (path36) {
6914
+ const path37 = process.env[PIWI_PROBE_ENV.plan];
6915
+ if (path37) {
6515
6916
  try {
6516
- planCache = JSON.parse(fs16.readFileSync(path36, "utf8"));
6917
+ planCache = JSON.parse(fs16.readFileSync(path37, "utf8"));
6517
6918
  } catch {
6518
6919
  planCache = null;
6519
6920
  }
@@ -6530,17 +6931,27 @@ function probeItemForTest(test) {
6530
6931
  function classifyProbeHandled(signals) {
6531
6932
  const blank = signals.blankPage === true;
6532
6933
  if ((signals.pageErrors ?? 0) > 0 || signals.backendError && blank) return "unhandled";
6533
- return signals.consoleErrors > 0 || signals.dialogs > 0 || signals.backendError || blank ? "degraded" : "graceful";
6934
+ const backendDegraded = signals.backendError && signals.faultIsTheError !== true;
6935
+ return signals.consoleErrors > 0 || signals.dialogs > 0 || backendDegraded || blank ? "degraded" : "graceful";
6936
+ }
6937
+ var SELF_ERROR_FAULTS = /* @__PURE__ */ new Set(["throw", "status"]);
6938
+ function faultIsTheError(fault) {
6939
+ return SELF_ERROR_FAULTS.has(fault);
6940
+ }
6941
+ var RESOURCE_ERROR_TEXT = /^Failed to load resource\b/;
6942
+ function probeConsoleErrors(entries) {
6943
+ return entries.filter((e) => e.type === "error" && !(typeof e.text === "string" && RESOURCE_ERROR_TEXT.test(e.text))).length;
6534
6944
  }
6535
6945
  function outcomeFromStatus(status, applied) {
6536
6946
  if (!applied) return "inconclusive";
6537
- return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
6947
+ if (status === "failed" || status === "timedOut") return "noticed";
6948
+ return status === "passed" ? "not-noticed" : "inconclusive";
6538
6949
  }
6539
6950
  function recordProbeOutcome(line) {
6540
- const path36 = process.env[PIWI_PROBE_ENV.results];
6541
- if (!path36) return;
6951
+ const path37 = process.env[PIWI_PROBE_ENV.results];
6952
+ if (!path37) return;
6542
6953
  try {
6543
- fs16.appendFileSync(path36, `${JSON.stringify(line)}
6954
+ fs16.appendFileSync(path37, `${JSON.stringify(line)}
6544
6955
  `);
6545
6956
  } catch {
6546
6957
  }
@@ -6830,23 +7241,26 @@ function dropOneField(body) {
6830
7241
  } catch {
6831
7242
  return body;
6832
7243
  }
7244
+ let dropped = false;
6833
7245
  if (Array.isArray(parsed2)) {
6834
7246
  let key;
6835
7247
  for (const el of parsed2) {
6836
7248
  if (el && typeof el === "object" && !Array.isArray(el)) {
6837
7249
  key ??= Object.keys(el)[0];
6838
- if (key) delete el[key];
7250
+ if (key && Object.prototype.hasOwnProperty.call(el, key)) {
7251
+ delete el[key];
7252
+ dropped = true;
7253
+ }
6839
7254
  }
6840
7255
  }
6841
- return JSON.stringify(parsed2);
6842
- }
6843
- if (parsed2 && typeof parsed2 === "object") {
6844
- const obj = parsed2;
6845
- const key = Object.keys(obj)[0];
6846
- if (key) delete obj[key];
6847
- return JSON.stringify(obj);
7256
+ } else if (parsed2 && typeof parsed2 === "object") {
7257
+ const key = Object.keys(parsed2)[0];
7258
+ if (key) {
7259
+ delete parsed2[key];
7260
+ dropped = true;
7261
+ }
6848
7262
  }
6849
- return body;
7263
+ return dropped ? JSON.stringify(parsed2) : body;
6850
7264
  }
6851
7265
  function computeFault(fault, input2) {
6852
7266
  const base = {
@@ -6923,7 +7337,7 @@ function originOf2(url) {
6923
7337
  return null;
6924
7338
  }
6925
7339
  }
6926
- var sleep = (ms) => new Promise((resolve11) => setTimeout(resolve11, Math.max(0, ms)));
7340
+ var sleep = (ms) => new Promise((resolve12) => setTimeout(resolve12, Math.max(0, ms)));
6927
7341
  async function performRouteAction(route, action, startedAt) {
6928
7342
  switch (action.kind) {
6929
7343
  case "abort":
@@ -7214,13 +7628,23 @@ function recordFlakeResult(line) {
7214
7628
  }
7215
7629
  }
7216
7630
 
7631
+ // src/internal/support/playwright-version.ts
7632
+ function playwrightVersionAtLeast(version, minimum) {
7633
+ const have = version ? /^(\d+)\.(\d+)/.exec(version) : null;
7634
+ const want = /^(\d+)\.(\d+)/.exec(minimum);
7635
+ if (!have || !want) return false;
7636
+ const major = Number(have[1]);
7637
+ const wantMajor = Number(want[1]);
7638
+ return major > wantMajor || major === wantMajor && Number(have[2]) >= Number(want[2]);
7639
+ }
7640
+
7217
7641
  // src/public/config-wrapper.ts
7218
7642
  var PIWI_MODULE = "@piwitests/reporter";
7219
7643
  var CAPTURE_DEFAULTS = {
7220
7644
  screenshot: "only-on-failure",
7221
7645
  trace: "retain-on-failure"
7222
7646
  };
7223
- var TRACE_SNAPSHOTS_MIN = { major: 1, minor: 63 };
7647
+ var TRACE_SNAPSHOTS_MIN = "1.63";
7224
7648
  function installedPlaywrightVersion() {
7225
7649
  try {
7226
7650
  const nodeRequire = require;
@@ -7230,18 +7654,11 @@ function installedPlaywrightVersion() {
7230
7654
  }
7231
7655
  }
7232
7656
  var readPlaywrightVersion = installedPlaywrightVersion;
7233
- function supportsTraceSnapshots(version) {
7234
- const match = version ? /^(\d+)\.(\d+)/.exec(version) : null;
7235
- if (!match) return false;
7236
- const major = Number(match[1]);
7237
- const minor = Number(match[2]);
7238
- return major > TRACE_SNAPSHOTS_MIN.major || major === TRACE_SNAPSHOTS_MIN.major && minor >= TRACE_SNAPSHOTS_MIN.minor;
7239
- }
7240
7657
  function applyCaptureDefaults(use, piwiOptions) {
7241
7658
  delete process.env[PIWI_DEFAULTED_CAPTURE_ENV];
7242
7659
  const enabled = piwiOptions?.defaultCapture ?? readBool(process.env[PIWI_ENV_KEYS.defaultCapture]) ?? true;
7243
7660
  if (!enabled) return use;
7244
- const ariaSnapshots = supportsTraceSnapshots(readPlaywrightVersion());
7661
+ const ariaSnapshots = playwrightVersionAtLeast(readPlaywrightVersion(), TRACE_SNAPSHOTS_MIN);
7245
7662
  const traceValue = ariaSnapshots ? { mode: CAPTURE_DEFAULTS.trace, snapshots: { dom: true, aria: true } } : CAPTURE_DEFAULTS.trace;
7246
7663
  const defaults = [
7247
7664
  { key: "screenshot", value: CAPTURE_DEFAULTS.screenshot, display: `screenshot: '${CAPTURE_DEFAULTS.screenshot}'` },
@@ -7308,79 +7725,6 @@ function wrapConfig(config, piwiOptions) {
7308
7725
  };
7309
7726
  }
7310
7727
 
7311
- // ../core/src/status-classify.ts
7312
- function mergeAnnotations(test, result) {
7313
- const out = [];
7314
- const seen = /* @__PURE__ */ new Set();
7315
- for (const list of [test.annotations, result.annotations]) {
7316
- for (const a of list ?? []) {
7317
- const key = `${a.type}\0${a.description ?? ""}`;
7318
- if (seen.has(key)) continue;
7319
- seen.add(key);
7320
- out.push(a.description === void 0 ? { type: a.type } : { type: a.type, description: a.description });
7321
- }
7322
- }
7323
- return out;
7324
- }
7325
- function expectsFailure(annotations) {
7326
- if (annotations.some((a) => a.type === "skip" || a.type === "fixme")) return false;
7327
- return annotations.some((a) => a.type === "fail");
7328
- }
7329
- var EXPECTED_FAILURE_PASSED_MESSAGE = "Expected to fail, but passed.";
7330
- function expectedFailureError(rawStatus, annotations) {
7331
- return rawStatus === "passed" && expectsFailure(annotations) ? EXPECTED_FAILURE_PASSED_MESSAGE : null;
7332
- }
7333
- function classifyStatus(rawStatus, annotations) {
7334
- if (rawStatus === "skipped") {
7335
- const intentional = annotations.some((a) => a.type === "skip" || a.type === "fixme");
7336
- return intentional ? "skipped" : "didnotrun";
7337
- }
7338
- if (expectsFailure(annotations)) {
7339
- if (rawStatus === "failed") return "passed";
7340
- if (rawStatus === "passed") return "failed";
7341
- }
7342
- return rawStatus;
7343
- }
7344
- function resolveUnrunReason(runStatus, opts) {
7345
- if (runStatus === "timedout") return "global-timeout";
7346
- if (opts.maxFailures > 0 && opts.failures >= opts.maxFailures) return "max-failures";
7347
- return "interrupted";
7348
- }
7349
- function fileOf(location) {
7350
- return location.replace(/:\d+:\d+$/, "");
7351
- }
7352
- function suiteKey(suitePath) {
7353
- return (suitePath ?? []).join("");
7354
- }
7355
- function sharedPrefixDepth(a, b) {
7356
- let depth = 0;
7357
- while (depth < a.length && depth < b.length && a[depth] === b[depth]) depth++;
7358
- return depth;
7359
- }
7360
- function linkBlockedTests(cases) {
7361
- const blockers = cases.filter((c) => c.status === "failed" || c.status === "timedOut" || c.status === "timedout");
7362
- if (blockers.length === 0) return;
7363
- for (const c of cases) {
7364
- if (c.didNotRunReason !== "previous-failure") continue;
7365
- const file = fileOf(c.location);
7366
- const inFile = blockers.filter((b) => fileOf(b.location) === file);
7367
- if (inFile.length === 0) continue;
7368
- const key = suiteKey(c.suitePath);
7369
- let blocker = inFile.find((b) => suiteKey(b.suitePath) === key);
7370
- if (!blocker) {
7371
- let bestDepth = 0;
7372
- for (const b of inFile) {
7373
- const depth = sharedPrefixDepth(c.suitePath ?? [], b.suitePath ?? []);
7374
- if (depth > bestDepth) {
7375
- bestDepth = depth;
7376
- blocker = b;
7377
- }
7378
- }
7379
- }
7380
- if (blocker) c.blockedBy = blocker.location;
7381
- }
7382
- }
7383
-
7384
7728
  // ../core/src/test-meta.ts
7385
7729
  var PIWI_ANNOTATION_PREFIX = "piwi:";
7386
7730
  var TEST_PRIORITIES = ["critical", "high", "medium", "low"];
@@ -7656,10 +8000,17 @@ function joinErrorMessages(errors) {
7656
8000
  const dedupeKey = msg.trim().slice(0, 200);
7657
8001
  if (seen.has(dedupeKey)) continue;
7658
8002
  seen.add(dedupeKey);
7659
- parts.push(msg);
8003
+ parts.push(hideSecretsInError(msg));
7660
8004
  }
7661
8005
  return parts.join("\n---\n") || null;
7662
8006
  }
8007
+ function locatedError(errors) {
8008
+ for (const error of errors ?? []) {
8009
+ const file = error?.location?.file;
8010
+ if (error && typeof file === "string" && file.length > 0) return error;
8011
+ }
8012
+ return void 0;
8013
+ }
7663
8014
  function appendErrorLocation(text2, location) {
7664
8015
  if (!location?.file || /\n[ \t]+at\s/.test(text2)) return text2;
7665
8016
  return `${text2}
@@ -7670,7 +8021,7 @@ function appendErrorLocation(text2, location) {
7670
8021
  function buildErrorText(result, base = process.cwd()) {
7671
8022
  const text2 = joinErrorMessages(result.errors);
7672
8023
  if (!text2) return null;
7673
- const loc = result.error?.location;
8024
+ const loc = locatedError([result.error, ...result.errors ?? []])?.location;
7674
8025
  if (!loc?.file) return text2;
7675
8026
  return appendErrorLocation(text2, { file: relativeTestPath(loc.file, base), line: loc.line, column: loc.column });
7676
8027
  }
@@ -7802,8 +8153,8 @@ function ciBuildUrlFromMetadata(metadata) {
7802
8153
 
7803
8154
  // src/internal/submit/run-submitter.ts
7804
8155
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
7805
- function markSubmitFallback(run, path36, reason) {
7806
- run.metadata.ingestHealth = { submitFallback: { path: path36, reason } };
8156
+ function markSubmitFallback(run, path37, reason) {
8157
+ run.metadata.ingestHealth = { submitFallback: { path: path37, reason } };
7807
8158
  }
7808
8159
  var RunSubmitter = class {
7809
8160
  /**
@@ -7888,7 +8239,7 @@ var RunSubmitter = class {
7888
8239
  if (sm?.enabled && sm?.runId != null) {
7889
8240
  if (sm.lostResults) fallbackReason = "results-lost";
7890
8241
  else {
7891
- outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
8242
+ outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth, counts.flakyTests);
7892
8243
  if (!outcome.done) fallbackReason = "finish-failed";
7893
8244
  }
7894
8245
  }
@@ -7958,15 +8309,16 @@ var RunSubmitter = class {
7958
8309
  deliveryKey: this.deliveryKey
7959
8310
  };
7960
8311
  }
7961
- async tryFinishStreaming(run, overallStatus, duration, auth) {
8312
+ async tryFinishStreaming(run, overallStatus, duration, auth, flakyTests) {
7962
8313
  const sm = this.streamManager;
7963
8314
  try {
7964
- const flakyTests = run.testCases.filter((tc) => tc.status === "passed" && (tc.retries || 0) > 0).length;
7965
8315
  const durations = run.testCases.filter((tc) => tc.duration != null).map((tc) => tc.duration);
7966
8316
  await sm.uploadRemaining(run.testCases);
7967
8317
  const finishBody = {
7968
8318
  streamToken: sm.token,
7969
8319
  status: overallStatus,
8320
+ // With `duration`, the span the run of a shard covers in its sharded run.
8321
+ startTime: run.startTime ?? void 0,
7970
8322
  duration,
7971
8323
  totalTests: run.totalTests,
7972
8324
  passedTests: run.passedTests,
@@ -7992,7 +8344,7 @@ var RunSubmitter = class {
7992
8344
  finishBody.shardTotal = run.shardInfo.total;
7993
8345
  }
7994
8346
  await this.httpClient.postJSON(`/api/test-runs/${sm.runId}/finish`, finishBody, auth);
7995
- this.logger.info(`Successfully finalized streaming run #${sm.runId}`);
8347
+ this.logger.debug(`Successfully finalized streaming run #${sm.runId}`);
7996
8348
  this.dropLocalCopies();
7997
8349
  if (this.hasReports(run)) {
7998
8350
  try {
@@ -8022,8 +8374,7 @@ var RunSubmitter = class {
8022
8374
  return { done: true, output: this.buildOutput(response?.runId, response?.projectId, run, overallStatus) };
8023
8375
  } catch (error) {
8024
8376
  if (error instanceof HttpError && error.status === 401 && !auth) return this.stopForMissingAuth(run, payload);
8025
- this.logger.warn(`Failed to upload with files: ${errorMessage(error)}`);
8026
- this.logger.info("Falling back to JSON upload...");
8377
+ this.logger.warn(`Could not upload the run's files (${errorMessage(error)}): sending its results alone.`);
8027
8378
  return { done: false, output: null };
8028
8379
  }
8029
8380
  }
@@ -9016,7 +9367,7 @@ function parseWindowsProcesses(text2) {
9016
9367
  }
9017
9368
  function roleOf(comm, cmdline) {
9018
9369
  const command = `${comm} ${cmdline}`;
9019
- if (/workerProcessEntry/.test(cmdline)) return "worker";
9370
+ if (/workerProcessEntry|[\\/]playwright[\\/]lib[\\/]common[\\/]process\.js/.test(cmdline)) return "worker";
9020
9371
  if (/loaderProcessEntry/.test(cmdline)) return "runner";
9021
9372
  if (/(^|[\s/])ffmpeg/.test(command)) return "ffmpeg";
9022
9373
  const type = /--type=([\w-]+)/.exec(cmdline)?.[1];
@@ -9469,11 +9820,21 @@ function runnableOf(info) {
9469
9820
  function fallbackRunnable(window) {
9470
9821
  return { phase: window ? "test" : "worker", fixture: null };
9471
9822
  }
9823
+ var GLOBALS_FILES = [
9824
+ ["lib", "globals.js"],
9825
+ ["lib", "common", "globals.js"]
9826
+ ];
9827
+ function playwrightGlobalsFile() {
9828
+ const packageJson = resolvePlaywrightPackageJson();
9829
+ if (!packageJson) return null;
9830
+ const root = path22.dirname(packageJson);
9831
+ return GLOBALS_FILES.map((parts) => path22.join(root, ...parts)).find((file) => fs21.existsSync(file)) ?? null;
9832
+ }
9472
9833
  function playwrightTestInfoSource() {
9473
9834
  try {
9474
- const packageJson = resolvePlaywrightPackageJson();
9475
- if (!packageJson) return () => null;
9476
- const globals = (0, import_node_module2.createRequire)(packageJson)(path22.join(path22.dirname(packageJson), "lib", "globals.js"));
9835
+ const file = playwrightGlobalsFile();
9836
+ if (!file) return () => null;
9837
+ const globals = (0, import_node_module2.createRequire)(file)(file);
9477
9838
  const read = globals.currentTestInfo;
9478
9839
  if (typeof read !== "function") return () => null;
9479
9840
  return () => {
@@ -9502,7 +9863,7 @@ function countHandles(types = TRACKED_HANDLE_TYPES) {
9502
9863
  async function settledHandles(start, types = TRACKED_HANDLE_TYPES) {
9503
9864
  const end = countHandles(types);
9504
9865
  if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
9505
- await new Promise((resolve11) => setTimeout(resolve11, 0));
9866
+ await new Promise((resolve12) => setTimeout(resolve12, 0));
9506
9867
  return countHandles(types);
9507
9868
  }
9508
9869
  function deepUserSite() {
@@ -9530,8 +9891,8 @@ function settleWithin(promise, ms) {
9530
9891
  let timer;
9531
9892
  return Promise.race([
9532
9893
  promise.catch(() => void 0),
9533
- new Promise((resolve11) => {
9534
- timer = setTimeout(resolve11, ms);
9894
+ new Promise((resolve12) => {
9895
+ timer = setTimeout(resolve12, ms);
9535
9896
  })
9536
9897
  ]).finally(() => clearTimeout(timer));
9537
9898
  }
@@ -9651,10 +10012,10 @@ var ResourceLedger = class {
9651
10012
  trackHandle(object, entry) {
9652
10013
  const info = this.testInfoSource();
9653
10014
  const { phase, fixture } = info ? runnableOf(info) : fallbackRunnable(this.window);
9654
- const tracked = { ...entry, phase, fixture, test: testRef2(info) ?? this.window };
9655
- this.handles.add(tracked);
9656
- this.byHandle.set(object, tracked);
9657
- return tracked;
10015
+ const tracked2 = { ...entry, phase, fixture, test: testRef2(info) ?? this.window };
10016
+ this.handles.add(tracked2);
10017
+ this.byHandle.set(object, tracked2);
10018
+ return tracked2;
9658
10019
  }
9659
10020
  onServer(server, site) {
9660
10021
  const known = this.byHandle.get(server);
@@ -9697,7 +10058,7 @@ var ResourceLedger = class {
9697
10058
  const own = () => [...this.handles].filter(
9698
10059
  (entry) => entry.test?.id === testId && !entry.fixture?.worker && (entry.phase === "test" || entry.phase === "beforeEach" || entry.phase === "afterEach")
9699
10060
  );
9700
- if (own().some((entry) => entry.pending?.())) await new Promise((resolve11) => setTimeout(resolve11, 0));
10061
+ if (own().some((entry) => entry.pending?.())) await new Promise((resolve12) => setTimeout(resolve12, 0));
9701
10062
  return own().filter((entry) => entry.running()).map((entry) => ({ type: entry.type, site: entry.site, ...entry.command ? { command: entry.command } : {} }));
9702
10063
  }
9703
10064
  /** Mark a page used: a locator was built on it. */
@@ -10174,10 +10535,10 @@ function buildResourceReport(input2) {
10174
10535
  const all = [];
10175
10536
  for (const censuses of byWorker.values()) {
10176
10537
  censuses.sort((a, b) => a.at - b.at);
10177
- const tracked = /* @__PURE__ */ new Map();
10538
+ const tracked2 = /* @__PURE__ */ new Map();
10178
10539
  for (const census of censuses) {
10179
10540
  for (const birth of census.born) {
10180
- tracked.set(birth.id, {
10541
+ tracked2.set(birth.id, {
10181
10542
  worker: census.worker,
10182
10543
  birth,
10183
10544
  bornCensus: census,
@@ -10189,13 +10550,13 @@ function buildResourceReport(input2) {
10189
10550
  });
10190
10551
  }
10191
10552
  for (const close of census.closed) {
10192
- const entry = tracked.get(close.id);
10553
+ const entry = tracked2.get(close.id);
10193
10554
  if (!entry) continue;
10194
10555
  entry.close = close;
10195
10556
  if (close.used !== void 0) entry.used = close.used;
10196
10557
  }
10197
10558
  for (const open of census.open) {
10198
- const entry = tracked.get(open.id);
10559
+ const entry = tracked2.get(open.id);
10199
10560
  if (!entry) continue;
10200
10561
  entry.lastOpenAt = census.at;
10201
10562
  if (open.used !== void 0) entry.used = open.used;
@@ -10213,7 +10574,7 @@ function buildResourceReport(input2) {
10213
10574
  }
10214
10575
  const testCensuses = censuses.filter((census) => census.test !== null);
10215
10576
  const workerEnded = censuses.some((census) => census.test === null);
10216
- for (const entry of tracked.values()) {
10577
+ for (const entry of tracked2.values()) {
10217
10578
  all.push(entry);
10218
10579
  if (entry.birth.kind === "page" && entry.used === false && !entry.bornCensus.test?.failed) idle.push(entry);
10219
10580
  const verdict = leakVerdict(entry, testCensuses, workerEnded);
@@ -10282,13 +10643,13 @@ function siteOf(birth) {
10282
10643
  var keyOf = (worker, id) => `${worker}:${id}`;
10283
10644
  function groupLeaks(leaked, all) {
10284
10645
  const leaks = new Map(leaked.map((leak) => [keyOf(leak.tracked.worker, leak.tracked.birth.id), leak]));
10285
- const tracked = new Map(all.map((entry) => [keyOf(entry.worker, entry.birth.id), entry]));
10646
+ const tracked2 = new Map(all.map((entry) => [keyOf(entry.worker, entry.birth.id), entry]));
10286
10647
  const groups = /* @__PURE__ */ new Map();
10287
10648
  const groupKey = (leak) => `${leak.scope}|${leak.tracked.birth.kind}|${whereOf(leak.tracked.birth)}`;
10288
10649
  for (const leak of leaked) {
10289
10650
  const { birth } = leak.tracked;
10290
10651
  if (birth.kind === "context" && birth.implicit) continue;
10291
- if (foldTarget(leak.tracked, leaks, tracked)) continue;
10652
+ if (foldTarget(leak.tracked, leaks, tracked2)) continue;
10292
10653
  const key = groupKey(leak);
10293
10654
  const group = groups.get(key) ?? {
10294
10655
  verdict: "leaked",
@@ -10314,7 +10675,7 @@ function groupLeaks(leaked, all) {
10314
10675
  }
10315
10676
  for (const leak of leaked) {
10316
10677
  if (leak.tracked.birth.kind !== "page") continue;
10317
- const root = foldTarget(leak.tracked, leaks, tracked);
10678
+ const root = foldTarget(leak.tracked, leaks, tracked2);
10318
10679
  const group = root ? groups.get(groupKey(root)) : void 0;
10319
10680
  if (!group) continue;
10320
10681
  group.pages = (group.pages ?? 0) + 1;
@@ -10325,11 +10686,11 @@ function groupLeaks(leaked, all) {
10325
10686
  function addCpu(group, cpuMs) {
10326
10687
  if (cpuMs > 0) group.afterTestCpuMs = (group.afterTestCpuMs ?? 0) + cpuMs;
10327
10688
  }
10328
- function foldTarget(entry, leaks, tracked) {
10689
+ function foldTarget(entry, leaks, tracked2) {
10329
10690
  let parentId = entry.birth.parent;
10330
10691
  while (parentId !== null) {
10331
10692
  const key = keyOf(entry.worker, parentId);
10332
- const parent = tracked.get(key);
10693
+ const parent = tracked2.get(key);
10333
10694
  if (!parent) return null;
10334
10695
  if (!(parent.birth.kind === "context" && parent.birth.implicit)) return leaks.get(key) ?? null;
10335
10696
  parentId = parent.birth.parent;
@@ -10667,17 +11028,17 @@ function nodeCpuTicks() {
10667
11028
  return { busy, idle, iowait: 0, steal: 0, total: busy + idle };
10668
11029
  }
10669
11030
  function defaultPs() {
10670
- return new Promise((resolve11, reject) => {
11031
+ return new Promise((resolve12, reject) => {
10671
11032
  (0, import_node_child_process2.execFile)(
10672
11033
  "ps",
10673
11034
  ["-A", "-o", "pid=,ppid=,rss=,time=,command="],
10674
11035
  { timeout: 2e3, maxBuffer: 8 * 1024 * 1024 },
10675
- (error, stdout) => error ? reject(error) : resolve11(stdout)
11036
+ (error, stdout) => error ? reject(error) : resolve12(stdout)
10676
11037
  );
10677
11038
  });
10678
11039
  }
10679
11040
  function defaultWindowsQuery() {
10680
- return new Promise((resolve11, reject) => {
11041
+ return new Promise((resolve12, reject) => {
10681
11042
  (0, import_node_child_process2.execFile)(
10682
11043
  "powershell.exe",
10683
11044
  [
@@ -10687,7 +11048,7 @@ function defaultWindowsQuery() {
10687
11048
  "[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"
10688
11049
  ],
10689
11050
  { timeout: 1e4, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
10690
- (error, stdout) => error ? reject(error) : resolve11(stdout)
11051
+ (error, stdout) => error ? reject(error) : resolve12(stdout)
10691
11052
  );
10692
11053
  });
10693
11054
  }
@@ -11452,7 +11813,7 @@ function resourceTimeline(samples, censuses) {
11452
11813
  let startedAt = hasSamples ? samples.startedAt : Infinity;
11453
11814
  for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
11454
11815
  const shift = hasSamples ? samples.startedAt - startedAt : 0;
11455
- const relative10 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
11816
+ const relative11 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
11456
11817
  const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
11457
11818
  const perWorker = Math.max(
11458
11819
  60,
@@ -11460,17 +11821,17 @@ function resourceTimeline(samples, censuses) {
11460
11821
  );
11461
11822
  return {
11462
11823
  startedAt,
11463
- cpuPct: hasSamples ? downsamplePoints(relative10(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
11464
- memoryBytes: hasSamples ? downsamplePoints(relative10(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
11824
+ cpuPct: hasSamples ? downsamplePoints(relative11(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
11825
+ memoryBytes: hasSamples ? downsamplePoints(relative11(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
11465
11826
  pages: workers.map(({ worker, events }) => {
11466
- const points = relative10(events, -startedAt);
11827
+ const points = relative11(events, -startedAt);
11467
11828
  const last = points.pop();
11468
11829
  return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
11469
11830
  }),
11470
11831
  workers: trees.map((tree) => ({
11471
11832
  worker: tree.worker,
11472
- cpuCores: downsamplePoints(relative10(tree.cpuCores, shift), perWorker, "mean"),
11473
- memoryBytes: downsamplePoints(relative10(tree.memoryBytes, shift), perWorker, "max")
11833
+ cpuCores: downsamplePoints(relative11(tree.cpuCores, shift), perWorker, "mean"),
11834
+ memoryBytes: downsamplePoints(relative11(tree.memoryBytes, shift), perWorker, "max")
11474
11835
  }))
11475
11836
  };
11476
11837
  }
@@ -11550,7 +11911,7 @@ var LiveResources = class {
11550
11911
  const cursors = this.reset ? /* @__PURE__ */ new Map() : this.sent;
11551
11912
  const sent2 = new Map(cursors);
11552
11913
  let fresh = false;
11553
- const take = (key, points, cap, merge) => {
11914
+ const take2 = (key, points, cap, merge) => {
11554
11915
  const out = since(points, cursors.get(key), cap, merge);
11555
11916
  const last = points[points.length - 1];
11556
11917
  if (out.length > 0 && last) {
@@ -11561,11 +11922,11 @@ var LiveResources = class {
11561
11922
  };
11562
11923
  const timeline = { startedAt, cpuPct: [], memoryBytes: [], pages: [], workers: [] };
11563
11924
  if (samples) {
11564
- timeline.cpuPct = take("cpu", samples.cpuPct, TIMELINE_SERIES_CAP, "mean");
11565
- timeline.memoryBytes = take("memory", samples.memoryBytes, TIMELINE_SERIES_CAP, "max");
11925
+ timeline.cpuPct = take2("cpu", samples.cpuPct, TIMELINE_SERIES_CAP, "mean");
11926
+ timeline.memoryBytes = take2("memory", samples.memoryBytes, TIMELINE_SERIES_CAP, "max");
11566
11927
  for (const tree of workerTreesByIndex(samples, censuses)) {
11567
- const cpuCores = take(`cores:${tree.worker}`, tree.cpuCores, TIMELINE_WORKER_POINTS_CAP, "mean");
11568
- const memoryBytes = take(`bytes:${tree.worker}`, tree.memoryBytes, TIMELINE_WORKER_POINTS_CAP, "max");
11928
+ const cpuCores = take2(`cores:${tree.worker}`, tree.cpuCores, TIMELINE_WORKER_POINTS_CAP, "mean");
11929
+ const memoryBytes = take2(`bytes:${tree.worker}`, tree.memoryBytes, TIMELINE_WORKER_POINTS_CAP, "max");
11569
11930
  if (cpuCores.length > 0 || memoryBytes.length > 0) {
11570
11931
  timeline.workers.push({ worker: tree.worker, cpuCores, memoryBytes });
11571
11932
  }
@@ -12175,7 +12536,7 @@ function browserFunctionsInWorker() {
12175
12536
  function browserFunctionCoverage(files) {
12176
12537
  return sanitizeCoverage({
12177
12538
  v: 1,
12178
- files: [...files].map(([path36, functions]) => ({ path: path36, runtime: "browser", functions }))
12539
+ files: [...files].map(([path37, functions]) => ({ path: path37, runtime: "browser", functions }))
12179
12540
  });
12180
12541
  }
12181
12542
  function newKnownFunctions(known, sent2) {
@@ -12188,19 +12549,19 @@ function newKnownFunctions(known, sent2) {
12188
12549
  budget = MAX_COVERAGE_RANGES;
12189
12550
  };
12190
12551
  for (const [raw, ranges] of known) {
12191
- const path36 = normalizeCoveragePath(raw);
12192
- if (path36 === null) continue;
12193
- let named = sent2.get(path36);
12552
+ const path37 = normalizeCoveragePath(raw);
12553
+ if (path37 === null) continue;
12554
+ let named = sent2.get(path37);
12194
12555
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
12195
12556
  if (fresh.length === 0) continue;
12196
- if (!named) sent2.set(path36, named = /* @__PURE__ */ new Set());
12557
+ if (!named) sent2.set(path37, named = /* @__PURE__ */ new Set());
12197
12558
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
12198
12559
  while (fresh.length > 0) {
12199
12560
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
12200
12561
  const part = fresh.slice(0, budget);
12201
12562
  fresh = fresh.slice(part.length);
12202
12563
  budget -= part.length;
12203
- files.push({ path: path36, allFunctions: part });
12564
+ files.push({ path: path37, allFunctions: part });
12204
12565
  }
12205
12566
  }
12206
12567
  close();
@@ -12563,13 +12924,13 @@ function repositoryRoot(from) {
12563
12924
  }
12564
12925
  function codeReachRoots(configDir, configured) {
12565
12926
  const repoRoot = repositoryRoot(configDir);
12566
- const resolve11 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path25.resolve(configDir, r));
12567
- const global = Array.isArray(configured) ? resolve11(configured) : resolve11(configured?.["*"]);
12927
+ const resolve12 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path25.resolve(configDir, r));
12928
+ const global = Array.isArray(configured) ? resolve12(configured) : resolve12(configured?.["*"]);
12568
12929
  const byOrigin = /* @__PURE__ */ new Map();
12569
12930
  if (configured && !Array.isArray(configured)) {
12570
12931
  for (const [key, dirs] of Object.entries(configured)) {
12571
12932
  const origin = key === "*" ? null : originOf3(key);
12572
- const resolved = resolve11(dirs);
12933
+ const resolved = resolve12(dirs);
12573
12934
  if (origin && resolved.length) byOrigin.set(origin, resolved);
12574
12935
  }
12575
12936
  }
@@ -12601,9 +12962,9 @@ function resolveSourcePath(source, roots) {
12601
12962
  const normalized = normalizeSourcePath(source);
12602
12963
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
12603
12964
  if (path25.isAbsolute(normalized) && isFile(normalized)) return path25.resolve(normalized);
12604
- const relative10 = normalized.replace(/^\/+/, "");
12965
+ const relative11 = normalized.replace(/^\/+/, "");
12605
12966
  for (const root of roots) {
12606
- const candidate = path25.resolve(root, relative10);
12967
+ const candidate = path25.resolve(root, relative11);
12607
12968
  if (isFile(candidate)) return candidate;
12608
12969
  }
12609
12970
  return null;
@@ -12692,10 +13053,10 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
12692
13053
  const files = /* @__PURE__ */ new Map();
12693
13054
  const known = /* @__PURE__ */ new Map();
12694
13055
  const addTo = (into, file, functions) => {
12695
- const path36 = relativeToRepo(file, roots.repoRoot);
12696
- const listed = into.get(path36);
13056
+ const path37 = relativeToRepo(file, roots.repoRoot);
13057
+ const listed = into.get(path37);
12697
13058
  if (listed) listed.push(...functions);
12698
- else into.set(path36, [...functions]);
13059
+ else into.set(path37, [...functions]);
12699
13060
  };
12700
13061
  const add2 = (file, functions) => addTo(files, file, functions);
12701
13062
  const bases = viteBases(entries);
@@ -12850,8 +13211,8 @@ function onScriptParsed(page, capture, event) {
12850
13211
  var READ_WAIT_MS = 5e3;
12851
13212
  function readCoverage(page, cdp) {
12852
13213
  let timer;
12853
- const late = new Promise((resolve11) => {
12854
- timer = setTimeout(() => resolve11([]), READ_WAIT_MS);
13214
+ const late = new Promise((resolve12) => {
13215
+ timer = setTimeout(() => resolve12([]), READ_WAIT_MS);
12855
13216
  });
12856
13217
  const taken = internalCall(page, () => cdp.send("Profiler.takePreciseCoverage")).then(
12857
13218
  (answer) => answer.result,
@@ -13047,14 +13408,14 @@ function namesLines(file) {
13047
13408
  }
13048
13409
  var GIT_BATCH = 500;
13049
13410
  function gitHashObjects(root, paths) {
13050
- return new Promise((resolve11) => {
13411
+ return new Promise((resolve12) => {
13051
13412
  const child = (0, import_node_child_process4.execFile)(
13052
13413
  "git",
13053
13414
  ["hash-object", "--stdin-paths"],
13054
13415
  { cwd: root, encoding: "utf8", timeout: 3e4, windowsHide: true },
13055
13416
  (error, stdout) => {
13056
13417
  const ids = error ? [] : stdout.split("\n").filter(Boolean);
13057
- resolve11(ids.length === paths.length ? ids : null);
13418
+ resolve12(ids.length === paths.length ? ids : null);
13058
13419
  }
13059
13420
  );
13060
13421
  child.stdin?.on("error", () => {
@@ -13352,7 +13713,7 @@ var CoverageCollection = class {
13352
13713
  }
13353
13714
  let timer;
13354
13715
  let finish;
13355
- const done = new Promise((resolve11) => {
13716
+ const done = new Promise((resolve12) => {
13356
13717
  let settled = false;
13357
13718
  finish = (server) => {
13358
13719
  if (settled) return;
@@ -13361,7 +13722,7 @@ var CoverageCollection = class {
13361
13722
  this.pending.delete(entry);
13362
13723
  void Promise.resolve(browser).then((side) => {
13363
13724
  this.assign(testCase, composeTestCoverage(server, side.files, this.options.settings, side.css));
13364
- resolve11();
13725
+ resolve12();
13365
13726
  });
13366
13727
  };
13367
13728
  });
@@ -13393,8 +13754,8 @@ var CoverageCollection = class {
13393
13754
  let timer;
13394
13755
  await Promise.race([
13395
13756
  Promise.all(waiting),
13396
- new Promise((resolve11) => {
13397
- timer = setTimeout(resolve11, boundMs);
13757
+ new Promise((resolve12) => {
13758
+ timer = setTimeout(resolve12, boundMs);
13398
13759
  })
13399
13760
  ]);
13400
13761
  clearTimeout(timer);
@@ -13457,6 +13818,15 @@ function testLocation(test, base) {
13457
13818
  function testFile(test, base) {
13458
13819
  return relativeTestPath(test.location.file, base);
13459
13820
  }
13821
+ function uncapturedCalls(steps) {
13822
+ let calls = false;
13823
+ const captured = (list, inHook) => list.some((step) => {
13824
+ if (step.category === "pw:api" && !inHook) calls = true;
13825
+ return step.category === "fixture" && /\bpiwiCapture\b/.test(step.title) || captured(step.steps, inHook || step.category === "hook");
13826
+ });
13827
+ return !captured(steps, false) && calls;
13828
+ }
13829
+ var MAX_UNCAPTURED_NAMED = 5;
13460
13830
  var PiwiDashboardReporter = class _PiwiDashboardReporter {
13461
13831
  constructor(rawOptions = {}) {
13462
13832
  this.testCases = [];
@@ -13473,6 +13843,10 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13473
13843
  this.plannedTests = [];
13474
13844
  /** Ids of tests that actually reported via `onTestEnd`, to find the ones that never ran. */
13475
13845
  this.reportedTestIds = /* @__PURE__ */ new Set();
13846
+ /** The spec files said to have run without the capture fixtures in a run that can stop (`noteUncaptured`). */
13847
+ this.uncapturedSpecs = /* @__PURE__ */ new Set();
13848
+ /** The live steps begun and not ended, by step, sent again when someone opens the run (`onWatched`). */
13849
+ this.openSteps = /* @__PURE__ */ new Map();
13476
13850
  /** Per-test attempt history, keyed by `test.id`; snapshotted onto every attempt's payload. */
13477
13851
  this.attemptsByTest = /* @__PURE__ */ new Map();
13478
13852
  /** The folder test files are named relative to: the Playwright config's, set in `onBegin`. */
@@ -13573,13 +13947,14 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13573
13947
  this.logger.debug("List mode (--list) detected \u2014 no run registered or report uploaded.");
13574
13948
  return;
13575
13949
  }
13576
- const followed = followEditor([configDirFromConfig(config), process.cwd()], {
13950
+ normalizeBoolEnv(process.env);
13951
+ const followed2 = followEditor([configDirFromConfig(config), process.cwd()], {
13577
13952
  env: process.env,
13578
13953
  argv: process.argv,
13579
13954
  terminal: !!process.stdout.isTTY || !!process.stderr.isTTY
13580
13955
  });
13581
- if (followed) {
13582
- const { editor } = followed;
13956
+ if (followed2) {
13957
+ const { editor } = followed2;
13583
13958
  this.logger.info(
13584
13959
  `Following this run in ${editor.client ?? "the editor"} (pid ${editor.pid}), whose breakpoints stop it: ${PIWI_EDITOR_ENV}=off runs it alone.`
13585
13960
  );
@@ -13622,11 +13997,13 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13622
13997
  const rawConfig = config;
13623
13998
  const grepRe = rawConfig.grep instanceof RegExp ? rawConfig.grep : void 0;
13624
13999
  const grepInvertRe = rawConfig.grepInvert instanceof RegExp ? rawConfig.grepInvert : void 0;
13625
- const grep = grepRe && grepRe.source !== ".*" ? grepRe.source : void 0;
13626
- const grepInvert = grepInvertRe?.source;
14000
+ const cliGrep = detectCliGrep();
14001
+ const grep = (grepRe && grepRe.source !== ".*" ? grepRe.source : void 0) ?? cliGrep.grep;
14002
+ const grepInvert = grepInvertRe?.source ?? cliGrep.grepInvert;
13627
14003
  const fileFilters = detectCliFileFilters();
14004
+ const narrowed = detectCliNarrowingFlags().length > 0;
13628
14005
  const selection = readSelectionStamp();
13629
- if (grep || grepInvert || fileFilters.length > 0 || selection) {
14006
+ if (grep || grepInvert || fileFilters.length > 0 || selection || narrowed) {
13630
14007
  this.isFullRun = false;
13631
14008
  this.filterDetails = {
13632
14009
  ...grep ? { grep } : {},
@@ -13634,7 +14011,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13634
14011
  ...fileFilters.length > 0 ? { files: fileFilters } : {},
13635
14012
  ...selection ? { selection } : {}
13636
14013
  };
13637
- this.logger.info(
14014
+ this.logger.debug(
13638
14015
  selection ? `Selection run detected (${selection.key})` : "Partial run detected (filter active)"
13639
14016
  );
13640
14017
  }
@@ -13697,6 +14074,12 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13697
14074
  // the dashboard shows the real total from the first render.
13698
14075
  this.plannedTests.length
13699
14076
  );
14077
+ if (this.streamManager) {
14078
+ this.streamManager.onWatched = () => {
14079
+ void this.liveResources?.tick();
14080
+ for (const event of this.openSteps.values()) this.streamManager?.queueEvent(event);
14081
+ };
14082
+ }
13700
14083
  if (this.streamManager && this.options.captureResources !== false) {
13701
14084
  const live = new LiveResources(
13702
14085
  this.streamManager,
@@ -13709,7 +14092,6 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13709
14092
  },
13710
14093
  this.logger
13711
14094
  );
13712
- this.streamManager.onWatched = () => void live.tick();
13713
14095
  live.start();
13714
14096
  this.liveResources = live;
13715
14097
  }
@@ -13718,18 +14100,16 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13718
14100
  * Name the run's live view on the instance for the workers, which Playwright starts after this hook with this
13719
14101
  * environment: the key the instance gave the global setup, for a project that turned its live view on, for this run
13720
14102
  * alone. A CI run's workers stream with it at once; a run outside CI's hold it until a person shares the run (Share
13721
- * live), and such a run without a key names why instead, for the surfaces that would share it. Read before the
13722
- * stream starts, which takes the setup file.
14103
+ * live), and such a run without a key names why instead, for the surfaces that would share it; a CI run without one
14104
+ * says why in its log (`ciLiveViewNote`). Read before the stream starts, which takes the setup file.
13723
14105
  */
13724
14106
  shareLiveView() {
13725
14107
  const ci = detectCi(process.env);
13726
14108
  const setup = this.streamManager ? peekSetupInfo(this.options.projectName) : null;
13727
14109
  if (!setup?.liveViewKey) {
13728
- if (!ci) {
13729
- process.env[LIVE_VIEW_ENV] = formatLiveViewMissing(
13730
- setup?.liveViewLocal ? "local" : setup ? "off" : "unregistered"
13731
- );
13732
- }
14110
+ const why = setup?.liveViewLocal ? "local" : setup ? "off" : "unregistered";
14111
+ if (!ci) process.env[LIVE_VIEW_ENV] = formatLiveViewMissing(why);
14112
+ else if (why !== "local") this.logger.info(ciLiveViewNote(this.streamManager ? why : "streaming-off"));
13733
14113
  return;
13734
14114
  }
13735
14115
  process.env[LIVE_VIEW_ENV] = formatLiveViewEnv(this.httpClient.baseUrl, setup.runId, setup.liveViewKey);
@@ -13817,6 +14197,33 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13817
14197
  }
13818
14198
  return true;
13819
14199
  }
14200
+ /**
14201
+ * In a run that can stop (breakpoints, or a run channel to an editor or the desktop app), say of the first spec file
14202
+ * whose test made Playwright calls without the capture fixtures, where the stops happen, that it never stops at a
14203
+ * breakpoint nor at Pause, which nothing else says; the others are named in one line as the run ends
14204
+ * (`noteOtherUncaptured`).
14205
+ */
14206
+ noteUncaptured(test, result) {
14207
+ if (!process.env.PIWI_PAUSE_AT && !process.env[RUN_CHANNEL_ENV]) return;
14208
+ if (!uncapturedCalls(result.steps)) return;
14209
+ const file = testFile(test, this.testPathBase);
14210
+ if (this.uncapturedSpecs.has(file)) return;
14211
+ this.uncapturedSpecs.add(file);
14212
+ if (this.uncapturedSpecs.size > 1) return;
14213
+ this.logger.warn(
14214
+ `${file} ran without Piwi's capture fixtures, so its tests never stop at a breakpoint or at Pause: import \`test\` from your fixtures file (the one that extends piwiFixtures) instead of '@playwright/test'.`
14215
+ );
14216
+ }
14217
+ /** The spec files past the first that ran without the capture fixtures, in one line (`noteUncaptured`). */
14218
+ noteOtherUncaptured() {
14219
+ const others = [...this.uncapturedSpecs].slice(1);
14220
+ if (!others.length) return;
14221
+ const named = others.slice(0, MAX_UNCAPTURED_NAMED).join(", ");
14222
+ const more = others.length > MAX_UNCAPTURED_NAMED ? `, and ${others.length - MAX_UNCAPTURED_NAMED} more` : "";
14223
+ this.logger.warn(
14224
+ `${others.length} more spec file${others.length === 1 ? "" : "s"} ran without Piwi's capture fixtures, so their tests never stop at a breakpoint or at Pause either: ${named}${more}.`
14225
+ );
14226
+ }
13820
14227
  /** Playwright reporter hook: called when a step (including hook/fixture) begins */
13821
14228
  onStepBegin(test, _result, step) {
13822
14229
  if (!this.enabled || !this.streamManager) return;
@@ -13824,7 +14231,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13824
14231
  const cat = step.category;
13825
14232
  const event = {
13826
14233
  type: "step-begin",
13827
- title: step.title,
14234
+ title: hideTypedStepTitle(step.title, cat),
13828
14235
  subtitle: typeof step.subtitle === "string" && step.subtitle.length > 0 ? step.subtitle : null,
13829
14236
  location: step.location ? `${step.location.file}:${step.location.line}:${step.location.column}` : "unknown",
13830
14237
  stepCategory: cat,
@@ -13832,18 +14239,20 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13832
14239
  workerIndex: workerIndexOf(_result),
13833
14240
  startedAt: step.startTime instanceof Date ? step.startTime.getTime() : null
13834
14241
  };
14242
+ this.openSteps.set(step, event);
13835
14243
  this.streamManager?.queueBeginEvent(event);
13836
14244
  }
13837
14245
  /** Playwright reporter hook: called when a step (including hook/fixture) ends */
13838
14246
  onStepEnd(test, _result, step) {
13839
14247
  if (!this.enabled || !this.streamManager) return;
13840
14248
  if (!_PiwiDashboardReporter.isLiveStep(step)) return;
14249
+ this.openSteps.delete(step);
13841
14250
  const cat = step.category;
13842
14251
  const workerIndex = workerIndexOf(_result);
13843
14252
  const startedAt = step.startTime instanceof Date ? step.startTime.getTime() : null;
13844
14253
  const event = {
13845
14254
  type: "step-end",
13846
- title: step.title,
14255
+ title: hideTypedStepTitle(step.title, cat),
13847
14256
  subtitle: typeof step.subtitle === "string" && step.subtitle.length > 0 ? step.subtitle : null,
13848
14257
  location: step.location ? `${step.location.file}:${step.location.line}:${step.location.column}` : "unknown",
13849
14258
  status: step.error ? "failed" : "passed",
@@ -13869,7 +14278,10 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13869
14278
  /** Playwright reporter hook: called when an individual test finishes */
13870
14279
  onTestEnd(test, result) {
13871
14280
  this.totalTests++;
14281
+ const worker = workerIndexOf(result);
14282
+ for (const [step, event] of this.openSteps) if (event.workerIndex === worker) this.openSteps.delete(step);
13872
14283
  this.reportedTestIds.add(test.id);
14284
+ this.noteUncaptured(test, result);
13873
14285
  const { suitePath, suiteConfig } = this.metadataCollector.getSuiteInfo(test);
13874
14286
  const annotations = mergeAnnotations(test, result);
13875
14287
  const status = classifyStatus(result.status, annotations);
@@ -13918,10 +14330,12 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
13918
14330
  expectedStatus: test.expectedStatus ?? null
13919
14331
  };
13920
14332
  if (result.status === "failed" || result.status === "timedOut") {
13921
- const failingLine = extractFailingLine(testCase.error, test.location.file, test.location.line);
14333
+ const failingLine = extractFailingLine(testCase.error, test.location.file, test.location.line, this.testPathBase);
13922
14334
  const snippet = readSourceSnippet(test.location.file, test.location.line, 30, failingLine);
13923
14335
  if (snippet) testCase.testSource = snippet;
13924
- const frames = collectSourceFrames(testCase.error, test.location.file, test.location.line);
14336
+ const frames = collectSourceFrames(testCase.error, test.location.file, test.location.line, {
14337
+ projectRoot: this.testPathBase
14338
+ });
13925
14339
  if (frames.length) testCase.testSourceFrames = frames;
13926
14340
  }
13927
14341
  if (this.options.collectPerformanceMetrics && result.steps?.length > 0) {
@@ -14136,6 +14550,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
14136
14550
  * ID and `externalId.onDuplicate` is `'fail'`.
14137
14551
  */
14138
14552
  async onEnd(result) {
14553
+ this.noteOtherUncaptured();
14139
14554
  const duplicates = this.externalIds?.duplicates.length ?? 0;
14140
14555
  const failsOnDuplicates = duplicates > 0 && resolveOnDuplicate(this.options.externalId?.onDuplicate).onDuplicate === "fail";
14141
14556
  const overridden = failsOnDuplicates && result?.status === "passed";
@@ -14278,7 +14693,7 @@ async function resolveSelection(options = {}) {
14278
14693
  }
14279
14694
 
14280
14695
  // src/internal/capture/capture-fixtures.ts
14281
- var path32 = __toESM(require("path"));
14696
+ var path33 = __toESM(require("path"));
14282
14697
  var import_node_zlib2 = require("zlib");
14283
14698
 
14284
14699
  // ../picker-dom/src/probe.ts
@@ -14737,8 +15152,8 @@ function installPickerOverlay(arg) {
14737
15152
  return root && root !== doc && root.host ? root.host : null;
14738
15153
  };
14739
15154
  const targetOf = (e) => {
14740
- const path36 = typeof e.composedPath === "function" ? e.composedPath() : null;
14741
- const first = path36 && path36.length ? path36[0] : null;
15155
+ const path37 = typeof e.composedPath === "function" ? e.composedPath() : null;
15156
+ const first = path37 && path37.length ? path37[0] : null;
14742
15157
  return first && first.nodeType === 1 ? first : e.target;
14743
15158
  };
14744
15159
  const buildChain = (raw) => {
@@ -15043,14 +15458,14 @@ function showAnchorPicker(arg) {
15043
15458
  } catch {
15044
15459
  shadowRoots.length = 0;
15045
15460
  }
15046
- const within3 = (node2, scope) => {
15461
+ const within4 = (node2, scope) => {
15047
15462
  if (scope === doc) return true;
15048
15463
  for (let n = node2; n; n = parentOf(n)) if (n === scope) return true;
15049
15464
  return false;
15050
15465
  };
15051
15466
  const queryAll = (scope, sel) => {
15052
15467
  const found = Array.from(scope.querySelectorAll(sel));
15053
- for (const sr of shadowRoots) if (within3(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
15468
+ for (const sr of shadowRoots) if (within4(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
15054
15469
  return found;
15055
15470
  };
15056
15471
  const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
@@ -15439,8 +15854,22 @@ function showPauseBar(arg) {
15439
15854
  cleanup();
15440
15855
  g.__piwiPauseState = choice;
15441
15856
  };
15857
+ const pageTakesEscape = (e) => {
15858
+ const path37 = typeof e.composedPath === "function" ? e.composedPath() : [e.target];
15859
+ const el = path37.find((node) => node && node.nodeType === 1);
15860
+ if (el && !bar.contains(el) && typeof el.closest === "function") {
15861
+ if (el.closest('dialog[open],[role="dialog"],[role="alertdialog"],[role="listbox"],[role="menu"]')) return true;
15862
+ const popup = '[aria-expanded="true"]:is([role="combobox"],[aria-haspopup]:not([aria-haspopup="false"]))';
15863
+ if (el.matches(popup)) return true;
15864
+ }
15865
+ try {
15866
+ return !!doc.querySelector("dialog:modal");
15867
+ } catch {
15868
+ return false;
15869
+ }
15870
+ };
15442
15871
  const onKey = (e) => {
15443
- if (e.key !== "Escape") return;
15872
+ if (e.key !== "Escape" || pageTakesEscape(e)) return;
15444
15873
  e.preventDefault();
15445
15874
  e.stopImmediatePropagation();
15446
15875
  done("resume");
@@ -15457,7 +15886,9 @@ function showPauseBar(arg) {
15457
15886
  ...arg.record ? [
15458
15887
  ["Record here", "record", "Record steps in this page: the editor writes them before this line", false]
15459
15888
  ] : [],
15460
- ...arg.failure ? [] : [["Finish", "finish", "Run on and pause no more in this test", false]],
15889
+ ...arg.failure ? [] : [
15890
+ ["Finish", "finish", "Run the test on without stopping at a breakpoint again in this attempt", false]
15891
+ ],
15461
15892
  ...arg.end && !arg.failure ? [
15462
15893
  ["End the test", "end", "Skip the rest of this test: it is reported as skipped", false]
15463
15894
  ] : []
@@ -15493,6 +15924,102 @@ function takePauseState() {
15493
15924
  return typeof state === "string" ? state : null;
15494
15925
  }
15495
15926
 
15927
+ // ../picker-dom/src/overlay-stop.ts
15928
+ function showStopHighlight(elements, arg) {
15929
+ const g = globalThis;
15930
+ const doc = g.document;
15931
+ const gone = g.__piwiStopHighlightsGone instanceof Set ? g.__piwiStopHighlightsGone : /* @__PURE__ */ new Set();
15932
+ g.__piwiStopHighlightsGone = gone;
15933
+ if (!doc || !doc.documentElement) return 0;
15934
+ const drawn = doc.getElementById(`__piwi_stop_highlight_${arg.id}`);
15935
+ const targets = gone.has(arg.id) ? [] : elements.slice(0, 500);
15936
+ const shownIn = (layer2) => Array.from(layer2.children).filter((box) => box.dataset.box && box.style.display !== "none").length;
15937
+ const raise = (layer2) => {
15938
+ if (!layer2.hasAttribute("popover")) return;
15939
+ try {
15940
+ if (layer2.matches(":popover-open")) layer2.hidePopover();
15941
+ layer2.showPopover();
15942
+ } catch {
15943
+ layer2.removeAttribute("popover");
15944
+ }
15945
+ };
15946
+ const before = drawn?.__piwiTargets;
15947
+ if (drawn && before && before.length === targets.length && before.every((el, i) => el === targets[i])) {
15948
+ if (targets.length) raise(drawn);
15949
+ drawn.__piwiPlace?.();
15950
+ return shownIn(drawn);
15951
+ }
15952
+ drawn?.remove();
15953
+ if (!targets.length) return 0;
15954
+ const Z = 2147483600;
15955
+ const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
15956
+ const layer = doc.createElement("div");
15957
+ layer.id = `__piwi_stop_highlight_${arg.id}`;
15958
+ layer.setAttribute("data-piwi-stop-highlight", arg.kind);
15959
+ layer.setAttribute("aria-hidden", "true");
15960
+ layer.setAttribute("popover", "manual");
15961
+ layer.style.cssText = `position:fixed;inset:auto;top:0;left:0;width:0;height:0;margin:0;padding:0;border:0;background:transparent;overflow:visible;pointer-events:none;z-index:${Z - 1};`;
15962
+ layer.__piwiTargets = targets;
15963
+ const look = arg.kind === "call" ? "border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55);" : "border:2px dashed #a78bfa;background:rgba(167,139,250,.15);border-radius:3px;box-shadow:0 0 0 1px rgba(255,255,255,.8);";
15964
+ const boxes = targets.map(() => {
15965
+ const box = doc.createElement("div");
15966
+ box.dataset.box = "1";
15967
+ box.style.cssText = `position:fixed;display:none;box-sizing:border-box;pointer-events:none;${look}`;
15968
+ layer.appendChild(box);
15969
+ return box;
15970
+ });
15971
+ const label = doc.createElement("div");
15972
+ label.style.cssText = `position:fixed;display:none;pointer-events:none;box-sizing:border-box;max-width:min(620px,92vw);background:#0b1120;color:#f9fafb;border:1px solid #7c3aed;border-radius:6px;padding:2px 7px;font:12px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 2px 10px rgba(0,0,0,.4);`;
15973
+ label.textContent = arg.label;
15974
+ layer.appendChild(label);
15975
+ doc.documentElement.appendChild(layer);
15976
+ raise(layer);
15977
+ const place = () => {
15978
+ let first = null;
15979
+ targets.forEach((el, i) => {
15980
+ const box = boxes[i];
15981
+ const rect = el && el.isConnected && typeof el.getBoundingClientRect === "function" ? el.getBoundingClientRect() : null;
15982
+ if (!rect || rect.width === 0 && rect.height === 0) {
15983
+ box.style.display = "none";
15984
+ return;
15985
+ }
15986
+ box.style.display = "block";
15987
+ box.style.left = `${rect.left - 2}px`;
15988
+ box.style.top = `${rect.top - 2}px`;
15989
+ box.style.width = `${rect.width + 4}px`;
15990
+ box.style.height = `${rect.height + 4}px`;
15991
+ first ??= rect;
15992
+ });
15993
+ if (first && arg.label) {
15994
+ label.style.display = "block";
15995
+ const height = label.offsetHeight || 20;
15996
+ const above = first.top - 2 - height - 4;
15997
+ label.style.left = `${Math.max(4, first.left - 2)}px`;
15998
+ label.style.top = `${above >= 4 ? above : first.bottom + 6}px`;
15999
+ } else {
16000
+ label.style.display = "none";
16001
+ }
16002
+ };
16003
+ layer.__piwiPlace = place;
16004
+ const follow = () => {
16005
+ if (!layer.isConnected) return;
16006
+ place();
16007
+ g.requestAnimationFrame(follow);
16008
+ };
16009
+ follow();
16010
+ return shownIn(layer);
16011
+ }
16012
+ function removeStopHighlight(id) {
16013
+ const g = globalThis;
16014
+ const gone = g.__piwiStopHighlightsGone instanceof Set ? g.__piwiStopHighlightsGone : /* @__PURE__ */ new Set();
16015
+ g.__piwiStopHighlightsGone = gone;
16016
+ gone.add(id);
16017
+ const layer = g.document?.getElementById(`__piwi_stop_highlight_${id}`);
16018
+ if (!layer) return false;
16019
+ layer.remove();
16020
+ return true;
16021
+ }
16022
+
15496
16023
  // ../picker-dom/src/anchor-alternatives.ts
15497
16024
  var cssIdSelector = (id) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id) ? `#${id}` : `[id="${id.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"]`;
15498
16025
  var ANCHOR_KIND_SCORES = { testid: 80, id: 74, labeledRole: 68, role: 62 };
@@ -15575,21 +16102,21 @@ var MAX_INDETERMINATE = 20;
15575
16102
  var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
15576
16103
  var LEVEL = `${STEP}(?:/${STEP})*`;
15577
16104
  var ELEMENT_PATH_RE = new RegExp(`^${LEVEL}(?:\\|${LEVEL})*$`);
15578
- function isElementPath(path36) {
15579
- return typeof path36 === "string" && path36.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path36);
16105
+ function isElementPath(path37) {
16106
+ return typeof path37 === "string" && path37.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path37);
15580
16107
  }
15581
16108
  var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
15582
16109
  function elementRef(v) {
15583
16110
  if (!v || typeof v !== "object") return null;
15584
- const { path: path36, label } = v;
15585
- if (!isElementPath(path36)) return null;
15586
- return { path: path36, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
16111
+ const { path: path37, label } = v;
16112
+ if (!isElementPath(path37)) return null;
16113
+ return { path: path37, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
15587
16114
  }
15588
16115
  function textPoint(v) {
15589
16116
  if (!v || typeof v !== "object") return null;
15590
- const { path: path36, offset } = v;
16117
+ const { path: path37, offset } = v;
15591
16118
  const at = offsetOf(offset);
15592
- return isElementPath(path36) && at !== null ? { path: path36, offset: at } : null;
16119
+ return isElementPath(path37) && at !== null ? { path: path37, offset: at } : null;
15593
16120
  }
15594
16121
  function sanitizePageInteraction(raw) {
15595
16122
  if (!raw || typeof raw !== "object") return {};
@@ -15892,9 +16419,9 @@ function unlessEnded(run, ended) {
15892
16419
  if (!ended) return run;
15893
16420
  return Promise.race([
15894
16421
  run,
15895
- new Promise((resolve11) => {
15896
- if (ended.aborted) resolve11(void 0);
15897
- else ended.addEventListener("abort", () => resolve11(void 0), { once: true });
16422
+ new Promise((resolve12) => {
16423
+ if (ended.aborted) resolve12(void 0);
16424
+ else ended.addEventListener("abort", () => resolve12(void 0), { once: true });
15898
16425
  })
15899
16426
  ]);
15900
16427
  }
@@ -16040,6 +16567,7 @@ function applyPickToSnapshots(snapshots, pick) {
16040
16567
  }
16041
16568
 
16042
16569
  // src/internal/capture/pause-at.ts
16570
+ var fs26 = __toESM(require("fs"));
16043
16571
  var path29 = __toESM(require("path"));
16044
16572
 
16045
16573
  // ../core/src/editor-send.ts
@@ -16067,9 +16595,12 @@ function isInsidePath(file) {
16067
16595
  }
16068
16596
 
16069
16597
  // src/internal/capture/pause-at.ts
16598
+ function lineKey(line, windows = process.platform === "win32") {
16599
+ return windows ? line.toLowerCase() : line;
16600
+ }
16070
16601
  var PauseSet = class {
16071
16602
  constructor(lines) {
16072
- this.lines = lines;
16603
+ this.lines = new Set([...lines].map((line) => lineKey(line)));
16073
16604
  }
16074
16605
  get size() {
16075
16606
  return this.lines.size;
@@ -16078,18 +16609,27 @@ var PauseSet = class {
16078
16609
  matches(callerLocation) {
16079
16610
  if (!callerLocation) return false;
16080
16611
  const col = callerLocation.lastIndexOf(":");
16081
- return col > 0 && this.lines.has(callerLocation.slice(0, col));
16612
+ return col > 0 && this.lines.has(lineKey(callerLocation.slice(0, col)));
16082
16613
  }
16083
16614
  };
16084
16615
  var NO_BREAKPOINTS = new PauseSet(/* @__PURE__ */ new Set());
16085
16616
  function callSiteFile(file, cwd) {
16086
16617
  const slashed = file.replace(/\\/g, "/");
16087
16618
  const absolute = path29.isAbsolute(file) || path29.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
16088
- let rel = absolute ? path29.relative(cwd, file) : path29.normalize(slashed);
16619
+ const resolved = path29.resolve(cwd, absolute ? file : slashed);
16620
+ const real = realPath(resolved);
16621
+ let rel = real !== null && real !== resolved ? path29.relative(realPath(cwd) ?? cwd, real) : absolute ? path29.relative(cwd, file) : path29.normalize(slashed);
16089
16622
  rel = rel.split(path29.sep).join("/").replace(/\\/g, "/");
16090
16623
  if (rel.startsWith("./")) rel = rel.slice(2);
16091
16624
  return rel;
16092
16625
  }
16626
+ function realPath(file) {
16627
+ try {
16628
+ return fs26.realpathSync(file);
16629
+ } catch {
16630
+ return null;
16631
+ }
16632
+ }
16093
16633
  function stopCallSite(location, cwd = process.cwd()) {
16094
16634
  if (!location.line) return null;
16095
16635
  return `${callSiteFile(location.file, cwd)}:${location.line}:${location.column ?? 0}`;
@@ -16150,10 +16690,20 @@ function callSitePlace(callSite) {
16150
16690
  function placeLine(place, cwd = process.cwd()) {
16151
16691
  return `${callSiteFile(place.file, cwd)}:${place.line}`;
16152
16692
  }
16153
- function pauseGateFromTestInfo(testInfo) {
16693
+ function pauseGateFromTestInfo(testInfo, headless) {
16154
16694
  const use = testInfo.project?.use ?? {};
16155
16695
  const debug = testInfo._configInternal?.configCLIOverrides?.debug;
16156
- return { ci: process.env.CI, headless: use.headless, pwdebug: process.env.PWDEBUG, debug };
16696
+ return {
16697
+ ci: process.env.CI,
16698
+ headless: headless !== void 0 ? headless : use.headless,
16699
+ pwdebug: process.env.PWDEBUG,
16700
+ debug
16701
+ };
16702
+ }
16703
+ function mayPause(gate) {
16704
+ if (process.env.PIWI_PAUSE_AT) return true;
16705
+ if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug)) return true;
16706
+ return typeof gate.debug === "string" && gate.debug.length > 0;
16157
16707
  }
16158
16708
  function pauseSkipReason(gate) {
16159
16709
  if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
@@ -16176,7 +16726,7 @@ function atBreakpoint(state, callSite) {
16176
16726
  }
16177
16727
  function atStop(state, callSite) {
16178
16728
  if (callSite) {
16179
- const reason = state.step ? "step" : state.pause ? "pause" : state.runTo !== null && callSiteLine(callSite) === state.runTo ? "run-to" : atBreakpoint(state, callSite) ? "breakpoint" : null;
16729
+ const reason = state.step ? "step" : state.pause ? "pause" : state.runTo !== null && lineKey(callSiteLine(callSite)) === lineKey(state.runTo) ? "run-to" : atBreakpoint(state, callSite) ? "breakpoint" : null;
16180
16730
  if (reason) {
16181
16731
  stopShown(state);
16182
16732
  return { show: reason };
@@ -16207,6 +16757,9 @@ function goOn(state, answer) {
16207
16757
  return wantsStops(state) ? "next" : "resume";
16208
16758
  }
16209
16759
  var ENDED_REASON = "Ended from Piwi";
16760
+ function takesEndSignal(version) {
16761
+ return playwrightVersionAtLeast(version, "1.62");
16762
+ }
16210
16763
  var OPTIONS_AT = {
16211
16764
  fill: 1,
16212
16765
  selectOption: 1,
@@ -16264,11 +16817,23 @@ function syncInspectorSwitch() {
16264
16817
  if (closed && process.env[NO_INSPECTOR_ENV] === void 0) {
16265
16818
  process.env[NO_INSPECTOR_ENV] = "1";
16266
16819
  inspectorSwitch.set = true;
16820
+ clearSwitchAtStop();
16267
16821
  } else if (!closed && inspectorSwitch.set) {
16268
- if (process.env[NO_INSPECTOR_ENV] === "1") delete process.env[NO_INSPECTOR_ENV];
16269
- inspectorSwitch.set = false;
16822
+ clearSwitch();
16270
16823
  }
16271
16824
  }
16825
+ function clearSwitch() {
16826
+ if (inspectorSwitch.set && process.env[NO_INSPECTOR_ENV] === "1") delete process.env[NO_INSPECTOR_ENV];
16827
+ inspectorSwitch.set = false;
16828
+ }
16829
+ var clearsAtStop = false;
16830
+ function clearSwitchAtStop() {
16831
+ if (clearsAtStop) return;
16832
+ clearsAtStop = true;
16833
+ process.prependListener("message", (message) => {
16834
+ if (message?.method === "__stop__") clearSwitch();
16835
+ });
16836
+ }
16272
16837
  function markPausesWrapped() {
16273
16838
  inspectorSwitch.wrapped = true;
16274
16839
  syncInspectorSwitch();
@@ -16376,9 +16941,9 @@ var BarIntents = class {
16376
16941
  /** Hand the bar an intent; false while it runs one, or holds one already. */
16377
16942
  offer(intent) {
16378
16943
  if (this.busy || this.queued) return false;
16379
- const take = this.take;
16944
+ const take2 = this.take;
16380
16945
  this.take = null;
16381
- if (take) take(intent);
16946
+ if (take2) take2(intent);
16382
16947
  else this.queued = intent;
16383
16948
  return true;
16384
16949
  }
@@ -16388,8 +16953,8 @@ var BarIntents = class {
16388
16953
  const queued = this.queued;
16389
16954
  this.queued = null;
16390
16955
  if (queued) return Promise.resolve(queued);
16391
- return new Promise((resolve11) => {
16392
- this.take = resolve11;
16956
+ return new Promise((resolve12) => {
16957
+ this.take = resolve12;
16393
16958
  });
16394
16959
  }
16395
16960
  /** The bar got its answer: it takes no intent until it waits again. */
@@ -16404,7 +16969,7 @@ async function within(run, ms) {
16404
16969
  try {
16405
16970
  return await Promise.race([
16406
16971
  run,
16407
- new Promise((resolve11) => timer = setTimeout(() => resolve11(void 0), ms))
16972
+ new Promise((resolve12) => timer = setTimeout(() => resolve12(void 0), ms))
16408
16973
  ]);
16409
16974
  } finally {
16410
16975
  clearTimeout(timer);
@@ -16437,13 +17002,68 @@ async function viewHere(page, probe, onPick, onRecord, ended, intents, onIntent,
16437
17002
  }
16438
17003
  }
16439
17004
  }
17005
+ var REDRAW_MS = 500;
17006
+ var stopHighlights = 0;
17007
+ var FRAME_HOP = " >> internal:control=enter-frame >> ";
17008
+ async function frameOf(locator) {
17009
+ const own = locator;
17010
+ if (!own._frame || typeof own._selector !== "string") return null;
17011
+ const at = own._selector.lastIndexOf(FRAME_HOP);
17012
+ if (at < 0) return { frame: own._frame, selector: own._selector };
17013
+ const [iframe] = await own._frame.locator(own._selector.slice(0, at)).first().elementHandles();
17014
+ if (!iframe) return null;
17015
+ try {
17016
+ const frame = await iframe.contentFrame();
17017
+ return frame ? { frame, selector: own._selector.slice(at + FRAME_HOP.length) } : null;
17018
+ } finally {
17019
+ await iframe.dispose().catch(() => void 0);
17020
+ }
17021
+ }
17022
+ function highlightAtStop(page, locator, what, label) {
17023
+ const id = `${what}-${++stopHighlights}`;
17024
+ const arg = { id, kind: what, label };
17025
+ const drawnIn = /* @__PURE__ */ new Set();
17026
+ let hidden2 = false;
17027
+ let timer;
17028
+ const draw = async () => {
17029
+ const target = await frameOf(locator);
17030
+ if (hidden2) return 0;
17031
+ if (!target) return locator.evaluateAll(showStopHighlight, arg);
17032
+ drawnIn.add(target.frame);
17033
+ return target.frame.locator(target.selector).evaluateAll(showStopHighlight, arg);
17034
+ };
17035
+ const again = () => {
17036
+ if (hidden2 || page.isClosed?.()) return;
17037
+ timer = setTimeout(
17038
+ () => void internalCall(page, draw).catch(() => 0).then(again),
17039
+ REDRAW_MS
17040
+ );
17041
+ timer.unref?.();
17042
+ };
17043
+ const first = internalCall(page, draw).catch(() => 0);
17044
+ void first.then(again);
17045
+ const removeIn = (frame) => internalCall(page, async () => frame.evaluate(removeStopHighlight, id)).catch(() => false);
17046
+ return {
17047
+ shown: first.then((boxes) => boxes > 0),
17048
+ hide: async () => {
17049
+ hidden2 = true;
17050
+ clearTimeout(timer);
17051
+ try {
17052
+ const frames = drawnIn.size ? [...drawnIn] : [page.mainFrame()];
17053
+ const removed = await within(Promise.all(frames.map(removeIn)), TAKE_DOWN_MS);
17054
+ return removed?.includes(true) ?? false;
17055
+ } catch {
17056
+ return false;
17057
+ }
17058
+ }
17059
+ };
17060
+ }
16440
17061
  async function pauseHere(page, point, probe, onPick, onRecord, ended, intents, onIntent) {
16441
- let shown2 = null;
17062
+ let highlight = null;
16442
17063
  try {
16443
- const highlight = point.target?.highlight;
16444
- if (typeof highlight === "function") {
16445
- const highlighted = Promise.resolve().then(() => highlight.call(point.target)).catch(() => null);
16446
- shown2 = await within(highlighted, HIGHLIGHT_MS) ?? null;
17064
+ if (point.target) {
17065
+ highlight = point.target.highlight();
17066
+ await within(highlight.shown, HIGHLIGHT_MS);
16447
17067
  }
16448
17068
  const arg = {
16449
17069
  place: pausePlace(point.location),
@@ -16502,17 +17122,10 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended, intents, o
16502
17122
  } catch {
16503
17123
  return "resume";
16504
17124
  } finally {
16505
- const takeDown = async () => {
16506
- try {
16507
- if (ended?.aborted) await page.evaluate(takePauseState);
16508
- const dispose = shown2?.dispose;
16509
- const hide = page.hideHighlight;
16510
- if (typeof dispose === "function") await dispose.call(shown2);
16511
- else if (typeof hide === "function") await hide.call(page);
16512
- } catch {
16513
- }
16514
- };
16515
- await within(takeDown(), TAKE_DOWN_MS);
17125
+ await Promise.all([
17126
+ ended?.aborted ? within(page.evaluate(takePauseState), TAKE_DOWN_MS).catch(() => void 0) : void 0,
17127
+ highlight?.hide()
17128
+ ]);
16516
17129
  }
16517
17130
  }
16518
17131
 
@@ -16558,6 +17171,12 @@ function locatorOn(page, source) {
16558
17171
  }
16559
17172
  }
16560
17173
 
17174
+ // src/internal/capture/aria-ref.ts
17175
+ function locatorOfRef(page, ref) {
17176
+ const getByRef = page.getByRef;
17177
+ return typeof getByRef === "function" ? getByRef.call(page, ref) : page.locator(`aria-ref=${ref}`);
17178
+ }
17179
+
16561
17180
  // src/internal/capture/pick-at.ts
16562
17181
  var LINE = /^(\s*)- ([a-z][\w-]*)(?: "(?:[^"\\]|\\.)*")?((?: \[[^\]\n]*\])*)(?::.*)?$/;
16563
17182
  var REF = /\[ref=(e\d+)\]/;
@@ -16597,18 +17216,16 @@ var PIWI_OVERLAY = '[id^="__piwi"], [id^="piwi-"][id$="-host"]';
16597
17216
  var MAX_UNDER = 24;
16598
17217
  var MAX_CHECKED = 8;
16599
17218
  var NORMALIZED_SCORE = 60;
16600
- var PICK_AT_NEEDS = "Pick on the image needs Playwright 1.64 or later (page.getByRef and the boxes of page.ariaSnapshot): update @playwright/test.";
17219
+ var PICK_AT_SINCE = "1.60";
17220
+ var PICK_AT_NEEDS = "Pick on the image needs Playwright 1.60 or later (the boxes of page.ariaSnapshot): update @playwright/test.";
16601
17221
  var NO_ELEMENT = "No element there: pick an element of the page, not its background.";
16602
17222
  var IN_IFRAME = "That point is in an iframe, whose elements Pick on the image does not reach: use Pick in the test's window.";
16603
- function canPickAt(page) {
16604
- return typeof page.getByRef === "function";
16605
- }
16606
17223
  var hitIn = ({ x, y, overlay, elements }) => {
16607
17224
  let scope = globalThis.document;
16608
17225
  let hit = null;
16609
17226
  for (let depth = 0; scope && depth < 32; depth++) {
16610
- const within3 = scope;
16611
- const top = within3.elementsFromPoint(x, y).find((el) => el.getRootNode?.() === within3 && !el.closest?.(overlay));
17227
+ const within4 = scope;
17228
+ const top = within4.elementsFromPoint(x, y).find((el) => el.getRootNode?.() === within4 && !el.closest?.(overlay));
16612
17229
  if (!top) break;
16613
17230
  hit = top;
16614
17231
  scope = top.shadowRoot ?? null;
@@ -16622,14 +17239,12 @@ var hitIn = ({ x, y, overlay, elements }) => {
16622
17239
  return -1;
16623
17240
  };
16624
17241
  async function pickAtPoint(page, probe, x, y) {
16625
- if (!canPickAt(page)) return { refusal: PICK_AT_NEEDS };
16626
17242
  const snapshot = await page.ariaSnapshot({ mode: "ai", boxes: true, timeout: 5e3 });
16627
17243
  if (!/\[box=/.test(snapshot)) return { refusal: PICK_AT_NEEDS };
16628
17244
  const under = boxesAt(snapshotBoxes(snapshot), x, y).slice(0, MAX_UNDER);
16629
- const byRef2 = (ref) => page.getByRef(ref);
16630
17245
  const handles = await Promise.all(
16631
17246
  under.map(
16632
- (box) => byRef2(box.ref).elementHandle({ timeout: 2e3 }).catch(() => null)
17247
+ (box) => locatorOfRef(page, box.ref).elementHandle({ timeout: 2e3 }).catch(() => null)
16633
17248
  )
16634
17249
  );
16635
17250
  try {
@@ -16646,7 +17261,7 @@ async function pickAtPoint(page, probe, x, y) {
16646
17261
  (el, overlay) => !!el.closest(overlay),
16647
17262
  PIWI_OVERLAY
16648
17263
  );
16649
- if (!piwi) return await rank(page, probe, byRef2(box.ref), handle);
17264
+ if (!piwi) return await rank(page, probe, locatorOfRef(page, box.ref), handle);
16650
17265
  }
16651
17266
  return { refusal: NO_ELEMENT };
16652
17267
  } finally {
@@ -16694,31 +17309,360 @@ async function rank(page, probe, locator, handle) {
16694
17309
  return { picked: { picked, alternatives: [picked, ...candidates.filter((c) => c !== found)], element } };
16695
17310
  }
16696
17311
 
17312
+ // src/internal/capture/live-mask.ts
17313
+ var import_node_module3 = require("module");
17314
+ var MASKED_SPACING_MS = 500;
17315
+ var FRESH_MS = 1e3;
17316
+ var READS_KEPT = 4;
17317
+ var MAX_FRAMES = 32;
17318
+ var PAD = 2;
17319
+ var SAME_WITHIN = 1;
17320
+ var FRAME_READ_MS = 1e3;
17321
+ var MAX_FAILED_READS = 3;
17322
+ var MASK_RGB = [96, 96, 96];
17323
+ var TOO_MANY_FRAMES = `This page has more than ${MAX_FRAMES} frames: the live view cannot check them all for secret fields, so no image of it goes.`;
17324
+ var NO_CODEC = "A secret field shows on this page, and this Playwright gives the reporter nothing to mask it with: no image goes while it shows.";
17325
+ var UNREAD = "The live view cannot read where the secret fields of this page are, so no image of it goes until it can.";
17326
+ var ZOOMED = "This page is zoomed in: no image of it goes until it is not, its secret fields shown larger than read.";
17327
+ var UNREADABLE_MASKS = "The project masks elements this reporter cannot read: no image goes. Update @piwitests/reporter.";
17328
+ var codec;
17329
+ function jpegCodec() {
17330
+ if (codec !== void 0) return codec;
17331
+ codec = null;
17332
+ try {
17333
+ const playwright = resolvePlaywrightPackageJson();
17334
+ const bundle = playwright ? (0, import_node_module3.createRequire)(playwright)("playwright-core/lib/utilsBundle") : null;
17335
+ const found = bundle?.jpegjs;
17336
+ if (typeof found?.decode === "function" && typeof found.encode === "function") codec = found;
17337
+ } catch {
17338
+ }
17339
+ return codec;
17340
+ }
17341
+ function maskedIn(frame, masks) {
17342
+ let locator = frame.locator(SECRET_FIELD_SELECTOR);
17343
+ for (const source of masks) {
17344
+ const own = locatorOn(frame, source);
17345
+ if (!own) throw new Error(`The locator ${source} cannot be built.`);
17346
+ locator = locator.or(own);
17347
+ }
17348
+ return locator;
17349
+ }
17350
+ var unbuilt = (source) => `The project masks ${source}, which this Playwright cannot find elements by: no image of the page goes.`;
17351
+ var near = (a, b) => Math.abs(a - b) <= SAME_WITHIN;
17352
+ function readFrameRects(page, frame, masks) {
17353
+ return internalCall(
17354
+ page,
17355
+ () => maskedIn(frame, masks).evaluateAll((elements) => {
17356
+ const view3 = globalThis;
17357
+ const visual = view3.visualViewport;
17358
+ const layout = view3.document.scrollingElement;
17359
+ const zoomed = !!visual && (Math.abs(visual.offsetLeft) > 0.5 || Math.abs(visual.offsetTop) > 0.5 || (layout ? visual.width < layout.clientWidth - 1 || visual.height < layout.clientHeight - 1 : visual.scale > 1.001));
17360
+ return {
17361
+ rects: elements.map((element) => {
17362
+ const rect = element.getBoundingClientRect();
17363
+ return [rect.x, rect.y, rect.width, rect.height];
17364
+ }).filter(([, , width, height]) => width > 0 && height > 0),
17365
+ width: view3.innerWidth,
17366
+ height: view3.innerHeight,
17367
+ zoomed
17368
+ };
17369
+ })
17370
+ );
17371
+ }
17372
+ async function childOrigin(page, frame, inner) {
17373
+ return internalCall(page, async () => {
17374
+ const element = await frame.frameElement();
17375
+ try {
17376
+ const outer = await element.boundingBox();
17377
+ if (!outer) return null;
17378
+ const own = await element.evaluate((iframe) => {
17379
+ const el = iframe;
17380
+ const style = globalThis.getComputedStyle(iframe);
17381
+ const pad = (side) => parseFloat(side) || 0;
17382
+ return {
17383
+ left: el.clientLeft + pad(style.paddingLeft),
17384
+ top: el.clientTop + pad(style.paddingTop),
17385
+ width: el.clientWidth - pad(style.paddingLeft) - pad(style.paddingRight),
17386
+ height: el.clientHeight - pad(style.paddingTop) - pad(style.paddingBottom),
17387
+ offsetWidth: el.offsetWidth,
17388
+ offsetHeight: el.offsetHeight
17389
+ };
17390
+ });
17391
+ const drawnAsLaidOut = near(outer.width, own.offsetWidth) && near(outer.height, own.offsetHeight);
17392
+ const contentIsViewport = near(own.width, inner.width) && near(own.height, inner.height);
17393
+ if (!drawnAsLaidOut || !contentIsViewport) return { whole: outer };
17394
+ return { x: outer.x + own.left, y: outer.y + own.top };
17395
+ } finally {
17396
+ await element.dispose();
17397
+ }
17398
+ });
17399
+ }
17400
+ async function within2(run, ms) {
17401
+ let timer;
17402
+ try {
17403
+ return await Promise.race([
17404
+ run,
17405
+ new Promise((resolve12) => timer = setTimeout(() => resolve12(void 0), ms))
17406
+ ]);
17407
+ } finally {
17408
+ clearTimeout(timer);
17409
+ }
17410
+ }
17411
+ async function wholeFrame(page, frame) {
17412
+ const box = await within2(
17413
+ internalCall(page, async () => {
17414
+ const element = await frame.frameElement();
17415
+ try {
17416
+ return await element.boundingBox();
17417
+ } finally {
17418
+ await element.dispose();
17419
+ }
17420
+ }),
17421
+ FRAME_READ_MS
17422
+ ).catch(() => void 0);
17423
+ if (box === void 0) return null;
17424
+ return { rects: box ? [[box.x, box.y, box.width, box.height]] : [], width: 0, height: 0 };
17425
+ }
17426
+ async function readFrame(page, frame, masks) {
17427
+ try {
17428
+ const read = await within2(readFrameRects(page, frame, masks), FRAME_READ_MS);
17429
+ if (!read) return frame === page.mainFrame() ? null : await wholeFrame(page, frame);
17430
+ if (!read.rects.length || frame === page.mainFrame()) return read;
17431
+ const origin = await childOrigin(page, frame, read);
17432
+ if (!origin) return { ...read, rects: [] };
17433
+ if ("whole" in origin) {
17434
+ const { x, y, width, height } = origin.whole;
17435
+ return { ...read, rects: [[x, y, width, height]] };
17436
+ }
17437
+ return {
17438
+ ...read,
17439
+ rects: read.rects.map(([left, top, width, height]) => [origin.x + left, origin.y + top, width, height])
17440
+ };
17441
+ } catch {
17442
+ return frame.isDetached() ? { rects: [], width: 0, height: 0 } : null;
17443
+ }
17444
+ }
17445
+ async function readBoxes(page, masks) {
17446
+ if (!masks) return UNREADABLE_MASKS;
17447
+ const frames = page.frames();
17448
+ if (frames.length > MAX_FRAMES) return TOO_MANY_FRAMES;
17449
+ for (const source of masks) if (!locatorOn(page.mainFrame(), source)) return unbuilt(source);
17450
+ const main = page.mainFrame();
17451
+ const read = await Promise.all(frames.map((frame) => readFrame(page, frame, masks)));
17452
+ if (read.some((frame) => frame === null)) return null;
17453
+ const top = read[frames.indexOf(main)];
17454
+ if (!top) return null;
17455
+ if (top.zoomed) return ZOOMED;
17456
+ return {
17457
+ boxes: read.flatMap(
17458
+ (frame) => frame.rects.map(([x, y, width, height]) => ({ x, y, width, height }))
17459
+ ),
17460
+ viewport: { width: top.width, height: top.height }
17461
+ };
17462
+ }
17463
+ function sameBoxes(a, b) {
17464
+ return a.length === b.length && a.every((box, i) => {
17465
+ const other = b[i];
17466
+ return Math.abs(box.x - other.x) <= SAME_WITHIN && Math.abs(box.y - other.y) <= SAME_WITHIN && Math.abs(box.width - other.width) <= SAME_WITHIN && Math.abs(box.height - other.height) <= SAME_WITHIN;
17467
+ });
17468
+ }
17469
+ function jpegSize(data) {
17470
+ for (let i = 2; i + 9 < data.length; ) {
17471
+ if (data[i] !== 255) return null;
17472
+ const marker2 = data[i + 1];
17473
+ if (marker2 >= 192 && marker2 <= 195) return { height: data.readUInt16BE(i + 5), width: data.readUInt16BE(i + 7) };
17474
+ i += 2 + data.readUInt16BE(i + 2);
17475
+ }
17476
+ return null;
17477
+ }
17478
+ function shrink(image, width, height) {
17479
+ const data = new Uint8Array(width * height * 4);
17480
+ const sx = image.width / width;
17481
+ const sy = image.height / height;
17482
+ for (let y = 0; y < height; y++) {
17483
+ const y0 = Math.floor(y * sy);
17484
+ const y1 = Math.max(y0 + 1, Math.min(image.height, Math.floor((y + 1) * sy)));
17485
+ for (let x = 0; x < width; x++) {
17486
+ const x0 = Math.floor(x * sx);
17487
+ const x1 = Math.max(x0 + 1, Math.min(image.width, Math.floor((x + 1) * sx)));
17488
+ let r = 0;
17489
+ let g = 0;
17490
+ let b = 0;
17491
+ for (let yy = y0; yy < y1; yy++) {
17492
+ for (let xx = x0; xx < x1; xx++) {
17493
+ const i = (yy * image.width + xx) * 4;
17494
+ r += image.data[i];
17495
+ g += image.data[i + 1];
17496
+ b += image.data[i + 2];
17497
+ }
17498
+ }
17499
+ const n = (y1 - y0) * (x1 - x0);
17500
+ const o = (y * width + x) * 4;
17501
+ data[o] = r / n;
17502
+ data[o + 1] = g / n;
17503
+ data[o + 2] = b / n;
17504
+ data[o + 3] = 255;
17505
+ }
17506
+ }
17507
+ return { width, height, data };
17508
+ }
17509
+ function paintBoxes(jpeg, frame, boxes, size, quality) {
17510
+ const image = jpeg.decode(frame.data, { useTArray: true, formatAsRGBA: true });
17511
+ const sx = image.width / frame.width;
17512
+ const sy = image.height / frame.height;
17513
+ for (const box of boxes) {
17514
+ const x0 = Math.max(0, Math.floor((box.x - PAD) * sx));
17515
+ const y0 = Math.max(0, Math.floor((box.y - PAD) * sy));
17516
+ const x1 = Math.min(image.width, Math.ceil((box.x + box.width + PAD) * sx));
17517
+ const y1 = Math.min(image.height, Math.ceil((box.y + box.height + PAD) * sy));
17518
+ for (let y = y0; y < y1; y++) {
17519
+ for (let x = x0; x < x1; x++) {
17520
+ const i = (y * image.width + x) * 4;
17521
+ image.data[i] = MASK_RGB[0];
17522
+ image.data[i + 1] = MASK_RGB[1];
17523
+ image.data[i + 2] = MASK_RGB[2];
17524
+ image.data[i + 3] = 255;
17525
+ }
17526
+ }
17527
+ }
17528
+ const fits = image.width <= size.width && image.height <= size.height;
17529
+ return jpeg.encode(fits ? image : shrink(image, size.width, size.height), quality).data;
17530
+ }
17531
+ async function measureFrame(page, frame) {
17532
+ try {
17533
+ const measured = await within2(
17534
+ internalCall(page, async () => {
17535
+ const fields = await frame.locator(SECRET_FIELD_SELECTOR).elementHandles();
17536
+ try {
17537
+ return await Promise.all(fields.map((field) => field.boundingBox()));
17538
+ } finally {
17539
+ await Promise.all(fields.map((field) => field.dispose().catch(() => void 0)));
17540
+ }
17541
+ }),
17542
+ FRAME_READ_MS
17543
+ );
17544
+ if (measured) return measured.filter((box) => !!box && box.width > 0 && box.height > 0);
17545
+ const whole = frame === page.mainFrame() ? null : await wholeFrame(page, frame);
17546
+ return whole && whole.rects.map(([x, y, width, height]) => ({ x, y, width, height }));
17547
+ } catch {
17548
+ return frame.isDetached() ? [] : null;
17549
+ }
17550
+ }
17551
+ async function secretBoxes(page) {
17552
+ const [read, measured] = await Promise.all([
17553
+ readBoxes(page, []),
17554
+ Promise.all(
17555
+ page.frames().slice(0, MAX_FRAMES).map((frame) => measureFrame(page, frame))
17556
+ )
17557
+ ]);
17558
+ if (!read || typeof read === "string" || measured.some((boxes) => !boxes)) return null;
17559
+ return { ...read, boxes: [...read.boxes, ...measured.flat()] };
17560
+ }
17561
+ async function maskedImage(page, options) {
17562
+ const before = await secretBoxes(page);
17563
+ if (!before) return null;
17564
+ const shot = await internalCall(
17565
+ page,
17566
+ () => page.screenshot({
17567
+ type: "jpeg",
17568
+ quality: options.quality,
17569
+ timeout: options.timeout,
17570
+ ...options.style ? { style: options.style } : {}
17571
+ })
17572
+ ).catch(() => null);
17573
+ if (!shot) return null;
17574
+ const after = await secretBoxes(page);
17575
+ if (!after || !sameBoxes(before.boxes, after.boxes)) return null;
17576
+ const { width, height } = after.viewport;
17577
+ if (!near(width, before.viewport.width) || !near(height, before.viewport.height)) return null;
17578
+ if (!after.boxes.length) return shot;
17579
+ const jpeg = jpegCodec();
17580
+ const size = jpegSize(shot);
17581
+ if (!jpeg || !size) return null;
17582
+ try {
17583
+ return paintBoxes(jpeg, { data: shot, width, height, at: Date.now() }, after.boxes, size, options.quality);
17584
+ } catch {
17585
+ return null;
17586
+ }
17587
+ }
17588
+ function maskSecretFields(page, options) {
17589
+ const reads = [];
17590
+ let maskedAt = -Infinity;
17591
+ let failed2 = 0;
17592
+ const read = async () => {
17593
+ const began = Date.now();
17594
+ const found = await readBoxes(page, options.masks());
17595
+ if (typeof found === "string") return found;
17596
+ if (!found) return ++failed2 >= MAX_FAILED_READS ? UNREAD : null;
17597
+ failed2 = 0;
17598
+ const done = { began, ended: Date.now(), ...found };
17599
+ reads.push(done);
17600
+ if (reads.length > READS_KEPT) reads.shift();
17601
+ return done;
17602
+ };
17603
+ const showsViewportOf = (frame, read2) => near(frame.width, read2.viewport.width) && near(frame.height, read2.viewport.height);
17604
+ const shown2 = (frame, boxes, size) => {
17605
+ const own = jpegSize(frame.data);
17606
+ const fits = own === null || own.width <= size.width && own.height <= size.height;
17607
+ const jpeg = jpegCodec();
17608
+ if (!boxes.length && (fits || !jpeg)) return frame.data.length <= options.maxBytes ? frame : null;
17609
+ if (!jpeg) return NO_CODEC;
17610
+ let data;
17611
+ try {
17612
+ data = paintBoxes(jpeg, frame, boxes, size, options.quality);
17613
+ } catch {
17614
+ return null;
17615
+ }
17616
+ if (!boxes.length) return data.length <= options.maxBytes ? { ...frame, data } : null;
17617
+ if (data.length > options.maxBytes) return null;
17618
+ maskedAt = Date.now();
17619
+ return { ...frame, data };
17620
+ };
17621
+ return {
17622
+ get resting() {
17623
+ return Date.now() - maskedAt < MASKED_SPACING_MS;
17624
+ },
17625
+ async screen(frame) {
17626
+ const before = [...reads].reverse().find((r) => r.ended <= frame.at && r.ended >= frame.at - FRESH_MS) ?? null;
17627
+ const after = await read();
17628
+ if (typeof after === "string" || !after) return after;
17629
+ const size = jpegSize(frame.data) ?? frame;
17630
+ if (before && sameBoxes(before.boxes, after.boxes) && showsViewportOf(frame, before) && showsViewportOf(frame, after)) {
17631
+ return shown2(frame, after.boxes, size);
17632
+ }
17633
+ if (page.isClosed()) return null;
17634
+ const at = Date.now();
17635
+ const shot = await internalCall(
17636
+ page,
17637
+ () => page.screenshot({ type: "jpeg", quality: options.quality, scale: "css", caret: "initial", timeout: 5e3 })
17638
+ ).catch(() => null);
17639
+ if (!shot) return null;
17640
+ const again = await read();
17641
+ if (typeof again === "string" || !again) return again;
17642
+ if (!sameBoxes(after.boxes, again.boxes)) return null;
17643
+ const { width, height } = again.viewport;
17644
+ if (!near(width, after.viewport.width) || !near(height, after.viewport.height)) return null;
17645
+ return shown2(
17646
+ { data: shot, width, height, at },
17647
+ again.boxes,
17648
+ fitViewSize({ width, height }, size.width, size.height)
17649
+ );
17650
+ }
17651
+ };
17652
+ }
17653
+
16697
17654
  // src/internal/capture/agent-page.ts
16698
17655
  var ACT_MS = 5e3;
16699
- var SECRET_FIELDS = [
16700
- 'input[type="password" i]',
16701
- ...SECRET_AUTOCOMPLETE.flatMap((token) => [
16702
- `input[autocomplete~="${token}" i]`,
16703
- `textarea[autocomplete~="${token}" i]`
16704
- ])
16705
- ].join(", ");
17656
+ var SECRET_FIELDS = SECRET_FIELD_SELECTOR;
16706
17657
  var BAR_HIDDEN = "#__piwi-pause-bar { display: none !important; }";
16707
- var AGENT_NEEDS = "An agent reads and acts on a stopped page with Playwright 1.64 or later (page.getByRef and the AI snapshot): update @playwright/test.";
16708
- function canActByRef(page) {
16709
- return typeof page.getByRef === "function";
16710
- }
16711
- function byRef(page, ref) {
16712
- return page.getByRef(ref);
16713
- }
16714
17658
  function failed(prefix, error) {
16715
17659
  const first = errorMessage(error).split("\n")[0].trim();
16716
17660
  return hideSecretsInText(`${prefix}: ${first}`).slice(0, 300);
16717
17661
  }
16718
- var MAX_FRAMES = 32;
17662
+ var MAX_FRAMES2 = 32;
16719
17663
  async function fieldSecrets(page) {
16720
17664
  const values = await Promise.all(
16721
- page.frames().slice(0, MAX_FRAMES).map(
17665
+ page.frames().slice(0, MAX_FRAMES2).map(
16722
17666
  (frame) => frame.locator(SECRET_FIELDS).evaluateAll((fields) => fields.map((field) => field.value)).catch(() => [])
16723
17667
  )
16724
17668
  );
@@ -16772,13 +17716,7 @@ async function readSnapshot(page, depth, image) {
16772
17716
  const cut = hidden2.length > MAX_AGENT_SNAPSHOT;
16773
17717
  let shot = null;
16774
17718
  if (image) {
16775
- const data = await page.screenshot({
16776
- type: "jpeg",
16777
- quality: 60,
16778
- style: BAR_HIDDEN,
16779
- mask: page.frames().slice(0, MAX_FRAMES).map((frame) => frame.locator(SECRET_FIELDS)),
16780
- timeout: ACT_MS
16781
- }).catch(() => null);
17719
+ const data = await maskedImage(page, { quality: 60, style: BAR_HIDDEN, timeout: ACT_MS });
16782
17720
  const viewport = page.viewportSize();
16783
17721
  if (data) shot = { mime: "image/jpeg", data: data.toString("base64"), ...viewport };
16784
17722
  }
@@ -16847,14 +17785,13 @@ async function ranked(page, probe, locator, handle, testIdAttribute) {
16847
17785
  }
16848
17786
  var NO_REF = (ref) => `No element of the stopped page has the ref ${ref}: take the page's snapshot again, its refs may have changed.`;
16849
17787
  async function handleOf(page, ref) {
16850
- return byRef(page, ref).elementHandle({ timeout: 2e3 }).catch(() => null);
17788
+ return locatorOfRef(page, ref).elementHandle({ timeout: 2e3 }).catch(() => null);
16851
17789
  }
16852
17790
  async function refLocators(page, probe, ref, testIdAttribute) {
16853
- if (!canActByRef(page)) return { ok: false, message: AGENT_NEEDS, element: null, locators: [] };
16854
17791
  const handle = await handleOf(page, ref);
16855
17792
  if (!handle) return { ok: false, message: NO_REF(ref), element: null, locators: [] };
16856
17793
  try {
16857
- const { element, locators } = await ranked(page, probe, byRef(page, ref), handle, testIdAttribute);
17794
+ const { element, locators } = await ranked(page, probe, locatorOfRef(page, ref), handle, testIdAttribute);
16858
17795
  const alone = locators.filter((l) => l.unique).length;
16859
17796
  return {
16860
17797
  ok: true,
@@ -16898,12 +17835,11 @@ var PLAYED = {
16898
17835
  select: "Selected an option of"
16899
17836
  };
16900
17837
  async function actOnRef(page, probe, act, testIdAttribute) {
16901
- if (!canActByRef(page)) return { ok: false, message: AGENT_NEEDS, element: null, locators: [] };
16902
17838
  const handle = await handleOf(page, act.ref);
16903
17839
  if (!handle) return { ok: false, message: NO_REF(act.ref), element: null, locators: [] };
16904
17840
  let before;
16905
17841
  try {
16906
- before = await ranked(page, probe, byRef(page, act.ref), handle, testIdAttribute);
17842
+ before = await ranked(page, probe, locatorOfRef(page, act.ref), handle, testIdAttribute);
16907
17843
  } catch (error) {
16908
17844
  return { ok: false, message: failed("The element could not be read", error), element: null, locators: [] };
16909
17845
  } finally {
@@ -16911,7 +17847,7 @@ async function actOnRef(page, probe, act, testIdAttribute) {
16911
17847
  }
16912
17848
  try {
16913
17849
  await page.evaluate(barLetsThrough, true).catch(() => void 0);
16914
- await play(byRef(page, act.ref), act.action, act.value);
17850
+ await play(locatorOfRef(page, act.ref), act.action, act.value);
16915
17851
  } catch (error) {
16916
17852
  return { ok: false, message: failed(`${PLAYED[act.action]} ${act.ref} failed`, error), ...before };
16917
17853
  } finally {
@@ -16955,7 +17891,7 @@ async function bindBrowser(browser, name, workspaceDir, on) {
16955
17891
  }
16956
17892
 
16957
17893
  // src/internal/capture/run-events.ts
16958
- var path30 = __toESM(require("path"));
17894
+ var path31 = __toESM(require("path"));
16959
17895
 
16960
17896
  // src/internal/capture/run-channel.ts
16961
17897
  var net3 = __toESM(require("net"));
@@ -17034,7 +17970,198 @@ async function keptResponse(id) {
17034
17970
  var http3 = __toESM(require("http"));
17035
17971
  var https3 = __toESM(require("https"));
17036
17972
 
17973
+ // src/internal/capture/live-call-log.ts
17974
+ var path30 = __toESM(require("path"));
17975
+ var TARGET_EVERY_MS = 1e3;
17976
+ var KEPT_LINES = 1e3;
17977
+ var MAX_LINE2 = 600;
17978
+ var MAX_TITLE = 500;
17979
+ var MAX_HEARD = 200;
17980
+ function callStreamOf(context) {
17981
+ const dbg = context.debugger;
17982
+ return typeof dbg?._enable === "function" && typeof dbg.on === "function" && typeof dbg.off === "function" ? dbg : null;
17983
+ }
17984
+ var heard = /* @__PURE__ */ new Map();
17985
+ var tracked = /* @__PURE__ */ new WeakSet();
17986
+ function trackCalls(playwright) {
17987
+ const instrumentation = playwright?._instrumentation;
17988
+ if (!playwright || tracked.has(playwright) || typeof instrumentation?.addListener !== "function") return;
17989
+ tracked.add(playwright);
17990
+ instrumentation.addListener({
17991
+ onApiCallBegin: (zone, channel) => {
17992
+ if (channel?.method === "__waitInfo__") return;
17993
+ const selector = channel?.params?.selector;
17994
+ heard.set(zone, {
17995
+ zone,
17996
+ selector: typeof selector === "string" && selector ? selector : null,
17997
+ began: Date.now()
17998
+ });
17999
+ if (heard.size > MAX_HEARD) heard.delete(heard.keys().next().value);
18000
+ },
18001
+ onApiCallEnd: (zone) => {
18002
+ heard.delete(zone);
18003
+ }
18004
+ });
18005
+ }
18006
+ function heardAs(id) {
18007
+ for (const call of heard.values()) if (call.zone.callId === id) return call;
18008
+ return null;
18009
+ }
18010
+ function userFile(file) {
18011
+ if (typeof file !== "string" || !file || /[\\/]node_modules[\\/]/.test(file)) return null;
18012
+ const relative11 = path30.relative(process.cwd(), file).split(path30.sep).join("/");
18013
+ return relative11 && !relative11.startsWith("..") && !path30.isAbsolute(relative11) ? relative11 : null;
18014
+ }
18015
+ var followed = /* @__PURE__ */ new WeakMap();
18016
+ function take(state, update) {
18017
+ if (typeof update?.id !== "string" || typeof update.title !== "string") return;
18018
+ const status = update.status === "running" ? "running" : update.status === "error" ? "failed" : "passed";
18019
+ let call = state.calls.get(update.id);
18020
+ if (!call) {
18021
+ const heardOne = heardAs(update.id);
18022
+ call = {
18023
+ id: update.id,
18024
+ title: hideSecretsInText(hideTypedText(update.title)).slice(0, MAX_TITLE),
18025
+ file: userFile(update.location?.file),
18026
+ line: typeof update.location?.line === "number" ? update.location.line : null,
18027
+ status,
18028
+ lines: [],
18029
+ point: null,
18030
+ error: null,
18031
+ began: heardOne?.began ?? Date.now(),
18032
+ ended: null,
18033
+ order: ++state.told,
18034
+ selector: heardOne?.selector ?? null,
18035
+ secretField: heardOne?.selector ? isSecretLocator(heardOne.selector) : false
18036
+ };
18037
+ state.calls.set(update.id, call);
18038
+ }
18039
+ if (Array.isArray(update.newLogEntries)) {
18040
+ for (const entry of update.newLogEntries) {
18041
+ if (typeof entry !== "string") continue;
18042
+ call.secretField ||= namesSecretField(entry);
18043
+ call.lines.push(hideSecretsInText(hideSecretsInLog(entry, call.secretField)).slice(0, MAX_LINE2));
18044
+ }
18045
+ if (call.lines.length > KEPT_LINES) call.lines.splice(0, call.lines.length - KEPT_LINES);
18046
+ }
18047
+ const point = update.actionPoint;
18048
+ if (point && Number.isFinite(point.x) && Number.isFinite(point.y)) {
18049
+ call.point = { x: point.x, y: point.y };
18050
+ }
18051
+ if (typeof update.error === "string" && update.error) {
18052
+ const first = update.error.split("\n")[0];
18053
+ call.error = hideSecretsInText(hideSecretsInLog(first, call.secretField)).slice(0, MAX_LINE2);
18054
+ }
18055
+ call.status = status;
18056
+ if (status !== "running" && call.ended === null) call.ended = Date.now();
18057
+ }
18058
+ function shownCall(state, since2) {
18059
+ let shown2 = null;
18060
+ for (const call of state.calls.values()) {
18061
+ if (call.ended !== null && call.ended < since2) continue;
18062
+ if (!shown2) {
18063
+ shown2 = call;
18064
+ continue;
18065
+ }
18066
+ const running2 = call.status === "running";
18067
+ if (running2 !== (shown2.status === "running") ? running2 : call.order > shown2.order) shown2 = call;
18068
+ }
18069
+ return shown2;
18070
+ }
18071
+ function prune(state) {
18072
+ let last = null;
18073
+ for (const call of state.calls.values()) if (!last || call.order > last.order) last = call;
18074
+ for (const [id, call] of state.calls) if (call.status !== "running" && call !== last) state.calls.delete(id);
18075
+ }
18076
+ async function followContext(context) {
18077
+ const stream = callStreamOf(context);
18078
+ if (!stream) return null;
18079
+ const state = { calls: /* @__PURE__ */ new Map(), told: 0, views: /* @__PURE__ */ new Set() };
18080
+ const listener = (updates) => {
18081
+ if (!Array.isArray(updates)) return;
18082
+ for (const update of updates) take(state, update);
18083
+ prune(state);
18084
+ for (const view3 of state.views) view3();
18085
+ };
18086
+ stream.on("apicallsupdated", listener);
18087
+ try {
18088
+ await internalCall(context, () => stream._enable());
18089
+ } catch {
18090
+ stream.off("apicallsupdated", listener);
18091
+ return null;
18092
+ }
18093
+ return state;
18094
+ }
18095
+ async function followCallLog(page, since2, onLog) {
18096
+ const context = page.context();
18097
+ let state = followed.get(context);
18098
+ if (!state) {
18099
+ state = followContext(context);
18100
+ followed.set(context, state);
18101
+ }
18102
+ const calls = await state;
18103
+ if (!calls) return null;
18104
+ let stopped = false;
18105
+ let reading = false;
18106
+ let target = null;
18107
+ const onThisPage = () => !page.isClosed() && context.pages().length === 1;
18108
+ const tell = () => {
18109
+ const shown2 = shownCall(calls, since2);
18110
+ if (stopped || !shown2) return;
18111
+ const here = onThisPage();
18112
+ onLog({
18113
+ id: shown2.id,
18114
+ call: { title: shown2.title, file: shown2.file, line: shown2.line },
18115
+ status: shown2.status,
18116
+ lines: compressCallLog(shown2.lines).slice(-MAX_LIVE_CALL_LOG_LINES),
18117
+ point: here ? shown2.point : null,
18118
+ target: here && shown2.status === "running" && target?.id === shown2.id ? target.matches : null,
18119
+ viewport: page.viewportSize(),
18120
+ ms: (shown2.ended ?? Date.now()) - shown2.began,
18121
+ error: shown2.error
18122
+ });
18123
+ };
18124
+ calls.views.add(tell);
18125
+ tell();
18126
+ const timer = setInterval(() => {
18127
+ const shown2 = shownCall(calls, since2);
18128
+ const selector = shown2?.selector;
18129
+ if (stopped || reading || !shown2 || shown2.status !== "running" || !selector || !onThisPage()) return;
18130
+ if (Date.now() - shown2.began < TARGET_EVERY_MS) return;
18131
+ reading = true;
18132
+ const framed = selector.includes("internal:control=enter-frame");
18133
+ void internalCall(
18134
+ page,
18135
+ () => page.locator(selector).evaluateAll((elements) => {
18136
+ const first = elements[0];
18137
+ if (!first) return { count: 0, box: null, enabled: null };
18138
+ const rect = first.getBoundingClientRect();
18139
+ const disabled = first.disabled === true || first.getAttribute("aria-disabled") === "true";
18140
+ return {
18141
+ count: elements.length,
18142
+ box: rect.width || rect.height ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null,
18143
+ enabled: !disabled
18144
+ };
18145
+ })
18146
+ ).then((matches) => {
18147
+ if (stopped || shownCall(calls, since2) !== shown2 || shown2.status !== "running") return;
18148
+ if (!matches.count && shown2.lines.some((line) => line.includes("locator resolved to"))) return;
18149
+ target = { id: shown2.id, matches: framed ? { ...matches, box: null } : matches };
18150
+ tell();
18151
+ }).catch(() => void 0).finally(() => {
18152
+ reading = false;
18153
+ });
18154
+ }, TARGET_EVERY_MS);
18155
+ timer.unref?.();
18156
+ return () => {
18157
+ stopped = true;
18158
+ clearInterval(timer);
18159
+ calls.views.delete(tell);
18160
+ };
18161
+ }
18162
+
17037
18163
  // src/internal/capture/page-view.ts
18164
+ var SCREENCAST_LAG_MS = 100;
17038
18165
  var HOLDERS = /* @__PURE__ */ new WeakMap();
17039
18166
  var TEST_SCREENCASTS = /* @__PURE__ */ new WeakSet();
17040
18167
  var OWN_SCREENCASTS = /* @__PURE__ */ new WeakMap();
@@ -17087,16 +18214,39 @@ async function openPageView(page, options) {
17087
18214
  let last = null;
17088
18215
  let sent2 = null;
17089
18216
  let refused = false;
18217
+ let screening = false;
18218
+ let screened = null;
17090
18219
  const interval = 1e3 / options.fps;
18220
+ const deliver = (frame) => {
18221
+ if (closed || !options.live()) return;
18222
+ refused = false;
18223
+ sent2 = frame;
18224
+ options.send(frame);
18225
+ };
18226
+ const refuse2 = (why) => {
18227
+ if (closed || refused || !options.live()) return;
18228
+ refused = true;
18229
+ sent2 = { refused: why };
18230
+ options.refuse(why);
18231
+ };
17091
18232
  const pacer = new LatestPacer({
17092
18233
  intervalMs: interval,
17093
18234
  ...options.spacing ? { spacing: options.spacing } : {},
17094
- busy: options.busy,
18235
+ busy: () => screening || options.busy(),
17095
18236
  send: (frame) => {
17096
18237
  if (!options.live()) return;
17097
- refused = false;
17098
- sent2 = frame;
17099
- options.send(frame);
18238
+ if (!options.screen) return deliver(frame);
18239
+ screening = true;
18240
+ screened = options.screen(frame).then(
18241
+ (shown2) => {
18242
+ if (typeof shown2 === "string") refuse2(shown2);
18243
+ else if (shown2) deliver(shown2);
18244
+ },
18245
+ () => void 0
18246
+ ).finally(() => {
18247
+ screening = false;
18248
+ screened = null;
18249
+ });
17100
18250
  }
17101
18251
  });
17102
18252
  let lighter = null;
@@ -17111,23 +18261,20 @@ async function openPageView(page, options) {
17111
18261
  ).then(
17112
18262
  (shot) => {
17113
18263
  if (closed) return;
17114
- if (shot.length <= options.maxFrameBytes) return pacer.offer({ data: shot, ...viewport });
17115
- if (refused || !options.live()) return;
17116
- refused = true;
17117
- sent2 = "refused";
17118
- options.refuse();
18264
+ if (shot.length <= options.maxFrameBytes) return pacer.offer({ data: shot, ...viewport, at: now });
18265
+ refuse2();
17119
18266
  },
17120
18267
  () => void 0
17121
18268
  ).finally(() => {
17122
18269
  lighter = null;
17123
18270
  });
17124
18271
  };
17125
- const onFrame = (data, width, height) => {
18272
+ const onFrame = (data, width, height, at) => {
17126
18273
  if (closed || last?.equals(data)) return;
17127
18274
  last = data;
17128
18275
  if (data.length > options.maxFrameBytes) return lighten();
17129
18276
  const named = isViewSize(width, height);
17130
- pacer.offer({ data, width: named ? width : viewport.width, height: named ? height : viewport.height });
18277
+ pacer.offer({ data, width: named ? width : viewport.width, height: named ? height : viewport.height, at });
17131
18278
  };
17132
18279
  const screencast = screencastOf(page);
17133
18280
  let end = null;
@@ -17136,11 +18283,12 @@ async function openPageView(page, options) {
17136
18283
  const shoot = async () => {
17137
18284
  shots = null;
17138
18285
  if (closed || page.isClosed()) return;
18286
+ const at = Date.now();
17139
18287
  const shot = await internalCall(
17140
18288
  page,
17141
18289
  () => page.screenshot({ type: "jpeg", quality: options.quality, scale: "css", caret: "initial", timeout: 5e3 })
17142
18290
  ).catch(() => null);
17143
- if (shot) onFrame(shot, viewport.width, viewport.height);
18291
+ if (shot) onFrame(shot, viewport.width, viewport.height, at);
17144
18292
  if (!closed) {
17145
18293
  shots = setTimeout(() => void shoot(), options.shotMs);
17146
18294
  shots.unref?.();
@@ -17173,7 +18321,9 @@ async function openPageView(page, options) {
17173
18321
  size: options.size(viewport),
17174
18322
  quality: options.quality,
17175
18323
  onFrame: (frame) => {
17176
- onFrame(frame.data, frame.viewportWidth, frame.viewportHeight);
18324
+ const now = Date.now();
18325
+ const at = typeof frame.timestamp === "number" && Number.isFinite(frame.timestamp) ? Math.min(frame.timestamp, now) - SCREENCAST_LAG_MS : Number.NEGATIVE_INFINITY;
18326
+ onFrame(frame.data, frame.viewportWidth, frame.viewportHeight, at);
17177
18327
  return acks.next();
17178
18328
  }
17179
18329
  })
@@ -17197,9 +18347,22 @@ async function openPageView(page, options) {
17197
18347
  },
17198
18348
  resend: () => {
17199
18349
  if (closed || !sent2 || !options.live()) return;
17200
- if (sent2 === "refused") options.refuse();
18350
+ if ("refused" in sent2) options.refuse(sent2.refused);
17201
18351
  else options.send(sent2);
17202
18352
  },
18353
+ refresh: () => {
18354
+ if (closed) return;
18355
+ sent2 = null;
18356
+ last = null;
18357
+ const at = Date.now();
18358
+ void internalCall(
18359
+ page,
18360
+ () => page.screenshot({ type: "jpeg", quality: options.quality, scale: "css", caret: "initial", timeout: 5e3 })
18361
+ ).then(
18362
+ (shot) => onFrame(shot, viewport.width, viewport.height, at),
18363
+ () => void 0
18364
+ );
18365
+ },
17203
18366
  close: async () => {
17204
18367
  closed = true;
17205
18368
  sent2 = null;
@@ -17207,7 +18370,7 @@ async function openPageView(page, options) {
17207
18370
  pacer.close();
17208
18371
  if (shots) clearTimeout(shots);
17209
18372
  holders.delete(holder);
17210
- await lighter;
18373
+ await Promise.all([lighter, screened]);
17211
18374
  const stop = end;
17212
18375
  end = null;
17213
18376
  if (stop && !page.isClosed()) await stop().catch(() => void 0);
@@ -17225,6 +18388,7 @@ var MAX_EVENT_TEXT = 64 * 1024;
17225
18388
  var MAX_STATES_WAITING = 8;
17226
18389
  var LIGHT_QUALITY = 30;
17227
18390
  var SHOT_MS = 1e3;
18391
+ var LOG_SPACING_MS = Math.ceil(1e3 / LIVE_CALL_LOGS_PER_SECOND);
17228
18392
  var VIEW_END_MS = 1e3;
17229
18393
  var GIVE_UP = /* @__PURE__ */ new Set([401, 403, 404, 410]);
17230
18394
  var TOO_HEAVY = `This page is too heavy for the live view: each image of it is over ${MAX_LIVE_FRAME / 1024} KB, a lighter one included.`;
@@ -17297,7 +18461,8 @@ function openLiveView(worker, env = process.env, ci = detectCi(env)) {
17297
18461
  address,
17298
18462
  id: { shard: resolveShardInfo(worker.config, env)?.current ?? null, worker: worker.workerIndex },
17299
18463
  client,
17300
- agent: new client.Agent({ keepAlive: true, maxSockets: 2 }),
18464
+ // The stream, and a frame, a call log and a stop or an end in flight at once: none waits behind another.
18465
+ agent: new client.Agent({ keepAlive: true, maxSockets: 4 }),
17301
18466
  stream: null,
17302
18467
  timer: null,
17303
18468
  retries: 0,
@@ -17305,13 +18470,20 @@ function openLiveView(worker, env = process.env, ci = detectCi(env)) {
17305
18470
  ended: false,
17306
18471
  refused: null,
17307
18472
  sharing: ci,
18473
+ ci,
17308
18474
  lost: null,
17309
18475
  answered: null,
17310
18476
  viewers: 0,
17311
18477
  states: Promise.resolve(),
17312
18478
  statesWaiting: 0,
17313
18479
  stop: null,
17314
- posting: false
18480
+ posting: false,
18481
+ log: null,
18482
+ logPosting: false,
18483
+ logSentAt: 0,
18484
+ logTimer: null,
18485
+ logRefused: false,
18486
+ masks: []
17315
18487
  };
17316
18488
  if (ci) connect(link);
17317
18489
  changed();
@@ -17361,9 +18533,9 @@ async function stopSharing() {
17361
18533
  own.viewers = 0;
17362
18534
  changed();
17363
18535
  }
17364
- function liveViewTestBegan(title, call) {
18536
+ function liveViewTestBegan(title, call, mayPause2 = false) {
17365
18537
  if (!link) return;
17366
- running = { title, page: null, call };
18538
+ running = { title, page: null, call, mayPause: mayPause2, began: Date.now() };
17367
18539
  }
17368
18540
  function liveViewPage(page) {
17369
18541
  if (!link || !running || running.page) return;
@@ -17389,7 +18561,7 @@ function liveViewStopped(stop) {
17389
18561
  };
17390
18562
  }
17391
18563
  function hideCallSecrets(call) {
17392
- return { ...call, title: hideSecretsInText(call.title) };
18564
+ return { ...call, title: hideSecretsInText(hideTypedText(call.title)) };
17393
18565
  }
17394
18566
  function postState(own, message) {
17395
18567
  if (!active(own) || own.statesWaiting >= MAX_STATES_WAITING) return;
@@ -17457,7 +18629,8 @@ function connect(own) {
17457
18629
  text2 = text2.slice(end + 2);
17458
18630
  const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
17459
18631
  const message = data ? parseInstanceToWorker(data) : null;
17460
- if (message) setViewers(own, message.count);
18632
+ if (message?.type === "masks") setMasks(own, message.locators);
18633
+ else if (message) setViewers(own, message.count);
17461
18634
  }
17462
18635
  });
17463
18636
  response.on("error", () => drop(null));
@@ -17497,11 +18670,19 @@ function setViewers(own, count) {
17497
18670
  return;
17498
18671
  }
17499
18672
  if (view) {
17500
- if (count > before) void view.opened.then((opened) => opened?.resend());
18673
+ if (count > before) {
18674
+ void view.opened.then((opened) => opened?.resend());
18675
+ if (own.log) postLog(own, own.log);
18676
+ }
17501
18677
  return;
17502
18678
  }
17503
18679
  startView();
17504
18680
  }
18681
+ function setMasks(own, locators) {
18682
+ if (JSON.stringify(locators) === JSON.stringify(own.masks)) return;
18683
+ own.masks = locators;
18684
+ if (own === link) void view?.opened.then((opened) => opened?.refresh());
18685
+ }
17505
18686
  function startView() {
17506
18687
  const own = link;
17507
18688
  const page = running?.page ?? null;
@@ -17511,6 +18692,11 @@ function startView() {
17511
18692
  return;
17512
18693
  }
17513
18694
  const live = () => view?.page === page && link === own && active(own) && own.viewers > 0;
18695
+ const mask = maskSecretFields(page, {
18696
+ masks: () => own.masks,
18697
+ quality: LIVE_FRAME_QUALITY,
18698
+ maxBytes: MAX_LIVE_FRAME
18699
+ });
17514
18700
  const opened = openPageView(page, {
17515
18701
  size: (viewport) => fitViewSize(viewport, LIVE_FRAME_WIDTH, LIVE_FRAME_HEIGHT),
17516
18702
  quality: LIVE_FRAME_QUALITY,
@@ -17519,28 +18705,39 @@ function startView() {
17519
18705
  maxFrameBytes: MAX_LIVE_FRAME,
17520
18706
  lightQuality: LIGHT_QUALITY,
17521
18707
  shotMs: SHOT_MS,
17522
- busy: () => own.posting,
18708
+ busy: () => own.posting || mask.resting,
17523
18709
  live,
18710
+ screen: (frame) => mask.screen(frame),
17524
18711
  send: (frame) => postFrame(own, frame),
17525
- refuse: () => void post(own, { type: "frame", ...own.id, refused: TOO_HEAVY })
18712
+ refuse: (why) => void post(own, { type: "frame", ...own.id, refused: why ?? TOO_HEAVY })
17526
18713
  }).catch(() => null);
17527
18714
  const onClose = () => {
17528
18715
  if (view?.page === page) void stopView();
17529
18716
  };
17530
18717
  page.once("close", onClose);
17531
- view = { page, opened, onClose };
18718
+ const following = own.ci && running && !running.mayPause && !own.logRefused ? followCallLog(page, running.began, (log) => {
18719
+ if (live()) postLog(own, log);
18720
+ }).catch(() => null) : null;
18721
+ view = { page, opened, onClose, following };
17532
18722
  }
17533
18723
  async function stopView() {
17534
18724
  const shown2 = view;
17535
18725
  view = null;
17536
18726
  if (!shown2) return;
17537
18727
  shown2.page.off("close", shown2.onClose);
18728
+ void shown2.following?.then((stop) => stop?.());
18729
+ const own = link;
18730
+ if (own) {
18731
+ own.log = null;
18732
+ if (own.logTimer) clearTimeout(own.logTimer);
18733
+ own.logTimer = null;
18734
+ }
17538
18735
  const closed = shown2.opened.then((opened) => opened?.close());
17539
18736
  let timer;
17540
18737
  await Promise.race([
17541
18738
  closed.catch(() => void 0),
17542
- new Promise((resolve11) => {
17543
- timer = setTimeout(resolve11, VIEW_END_MS);
18739
+ new Promise((resolve12) => {
18740
+ timer = setTimeout(resolve12, VIEW_END_MS);
17544
18741
  timer.unref?.();
17545
18742
  })
17546
18743
  ]);
@@ -17561,19 +18758,47 @@ function postFrame(own, frame) {
17561
18758
  own.posting = false;
17562
18759
  });
17563
18760
  }
18761
+ function postLog(own, log) {
18762
+ own.log = log;
18763
+ nextLog(own);
18764
+ }
18765
+ function nextLog(own) {
18766
+ if (own.logPosting || own.logTimer) return;
18767
+ const wait = LOG_SPACING_MS - (Date.now() - own.logSentAt);
18768
+ if (wait > 0) {
18769
+ own.logTimer = setTimeout(() => {
18770
+ own.logTimer = null;
18771
+ nextLog(own);
18772
+ }, wait);
18773
+ own.logTimer.unref?.();
18774
+ return;
18775
+ }
18776
+ const log = own.log;
18777
+ if (!active(own) || !view || !log || own.logRefused) return;
18778
+ own.logPosting = true;
18779
+ own.logSentAt = Date.now();
18780
+ let again = false;
18781
+ void post(own, { type: "call-log", ...own.id, log, test: running?.title ?? null }).then((status) => {
18782
+ if (status === 400) own.logRefused = true;
18783
+ again = status === 429;
18784
+ }).catch(() => void 0).finally(() => {
18785
+ own.logPosting = false;
18786
+ if (own.log && (own.log !== log || again)) nextLog(own);
18787
+ });
18788
+ }
17564
18789
  function post(own, message) {
17565
18790
  return active(own) ? send(own, message) : Promise.resolve(null);
17566
18791
  }
17567
18792
  function send(own, message) {
17568
18793
  if (own.ended || own.refused !== null) return Promise.resolve(null);
17569
18794
  const body = JSON.stringify(message);
17570
- return new Promise((resolve11) => {
18795
+ return new Promise((resolve12) => {
17571
18796
  let settled = false;
17572
18797
  const settle = (status) => {
17573
18798
  if (settled) return;
17574
18799
  settled = true;
17575
18800
  if (status !== null && GIVE_UP.has(status)) refuse(own, status);
17576
- resolve11(status);
18801
+ resolve12(status);
17577
18802
  };
17578
18803
  const request3 = own.client.request(own.address.url, {
17579
18804
  method: "POST",
@@ -17600,7 +18825,6 @@ function send(own, message) {
17600
18825
  // src/internal/capture/run-channel.ts
17601
18826
  var CONNECT_MS = 2e3;
17602
18827
  var BAR_HIDDEN2 = "#__piwi-pause-bar { display: none !important; }";
17603
- var OUTLINE_STYLE = "outline: 2px dashed #a78bfa; outline-offset: 2px; background: rgba(167, 139, 250, 0.15);";
17604
18828
  var WENT_ON = "The test went on.";
17605
18829
  var MAX_FRAME_BACKLOG = 1024 * 1024;
17606
18830
  var SHOT_MS2 = 500;
@@ -17608,6 +18832,7 @@ var LIGHT_QUALITY2 = 30;
17608
18832
  var TOO_HEAVY2 = `This page is too heavy for the live view: each image of it is over ${MAX_VIEW_FRAME / 1024} KB, a lighter one included. The image of the stop still shows it.`;
17609
18833
  var VIEW_END_MS2 = 1e3;
17610
18834
  var PAGE_ANSWER_MS = 1e4;
18835
+ var OUTLINE_MS = 2e3;
17611
18836
  var NO_ANSWER = "The page did not answer in time.";
17612
18837
  var LATE_ACT = "The page did not answer in time: the action may still take effect, and is told when it does.";
17613
18838
  var MAX_INPUT_WAITING = 4;
@@ -17647,11 +18872,12 @@ var sharedHere = false;
17647
18872
  var liveSaid = "";
17648
18873
  var unhearLive = null;
17649
18874
  var NO_AGENT_PAGE = "This stop has no page an agent can read: its call does not name one among its context's pages.";
17650
- function workerTakes(env = process.env) {
18875
+ function workerTakes(env = process.env, playwright) {
17651
18876
  const pairing = !!env[PIWI_LOCAL_DEBUG_ENV.editorSend];
17652
18877
  const recorder = parseRecordChannel(env[RECORD_CHANNEL_ENV]) !== null;
18878
+ const boxes = playwright === void 0 || playwrightVersionAtLeast(playwright, PICK_AT_SINCE);
17653
18879
  return RUN_TAKES.filter(
17654
- (take) => take === "pick" ? pairing : take === "record" || take === "assert" ? recorder : true
18880
+ (take2) => take2 === "pick" ? pairing : take2 === "record" || take2 === "assert" ? recorder : take2 === "pick-at" ? boxes : true
17655
18881
  );
17656
18882
  }
17657
18883
  async function openRunChannel(worker, handlers, env = process.env) {
@@ -17660,15 +18886,15 @@ async function openRunChannel(worker, handlers, env = process.env) {
17660
18886
  if (socket || !channel || !ref || !/^[\w-]{1,64}$/.test(ref)) return false;
17661
18887
  const opening = net3.connect(channel.path);
17662
18888
  opening.setEncoding("utf8");
17663
- const opened = await new Promise((resolve11) => {
17664
- const timer = setTimeout(() => resolve11(false), CONNECT_MS);
18889
+ const opened = await new Promise((resolve12) => {
18890
+ const timer = setTimeout(() => resolve12(false), CONNECT_MS);
17665
18891
  opening.once("connect", () => {
17666
18892
  clearTimeout(timer);
17667
- resolve11(true);
18893
+ resolve12(true);
17668
18894
  });
17669
18895
  opening.once("error", () => {
17670
18896
  clearTimeout(timer);
17671
- resolve11(false);
18897
+ resolve12(false);
17672
18898
  });
17673
18899
  });
17674
18900
  if (!opened || opening.destroyed) {
@@ -17711,8 +18937,10 @@ async function openRunChannel(worker, handlers, env = process.env) {
17711
18937
  pid: process.pid,
17712
18938
  playwright: worker.config.version,
17713
18939
  reporter: getReporterVersion(),
17714
- takes: workerTakes(env),
17715
- holds: [...RUN_HOLDS]
18940
+ takes: workerTakes(env, worker.config.version),
18941
+ holds: [...RUN_HOLDS],
18942
+ // A run the reporter follows from outside the editor, which its own test runner started.
18943
+ ...ref.startsWith(TERMINAL_REF_PREFIX) && startedByTestRunner(env) ? { runner: true } : {}
17716
18944
  });
17717
18945
  liveSaid = "";
17718
18946
  tellLive();
@@ -17785,7 +19013,7 @@ async function unbind() {
17785
19013
  const shared = bound?.browser;
17786
19014
  bound = null;
17787
19015
  if (shared?.isConnected()) {
17788
- await within2(
19016
+ await within3(
17789
19017
  internalCall(shared, () => bindBrowser(shared, runName, process.cwd(), false)),
17790
19018
  VIEW_END_MS2
17791
19019
  );
@@ -17845,7 +19073,7 @@ function viewEnded(stop) {
17845
19073
  else viewEnds.add(() => ended.abort());
17846
19074
  return ended.signal;
17847
19075
  }
17848
- async function within2(run, ms) {
19076
+ async function within3(run, ms) {
17849
19077
  await answerWithin(run, ms, void 0);
17850
19078
  }
17851
19079
  async function answerWithin(run, ms, late) {
@@ -17853,20 +19081,20 @@ async function answerWithin(run, ms, late) {
17853
19081
  try {
17854
19082
  return await Promise.race([
17855
19083
  run.catch(() => late),
17856
- new Promise((resolve11) => timer = setTimeout(() => resolve11(late), ms))
19084
+ new Promise((resolve12) => timer = setTimeout(() => resolve12(late), ms))
17857
19085
  ]);
17858
19086
  } finally {
17859
19087
  clearTimeout(timer);
17860
19088
  }
17861
19089
  }
17862
19090
  async function endView(stop, page) {
17863
- if (viewStarting?.stop === stop) await within2(viewStarting.started, VIEW_END_MS2);
19091
+ if (viewStarting?.stop === stop) await within3(viewStarting.started, VIEW_END_MS2);
17864
19092
  if (view2?.stop === stop) await stopView2();
17865
19093
  const own = input;
17866
19094
  input = freshInput();
17867
- await within2(own.playing, VIEW_END_MS2);
19095
+ await within3(own.playing, VIEW_END_MS2);
17868
19096
  if (page && !page.isClosed() && (own.buttons.size || own.keys.size)) {
17869
- await within2(
19097
+ await within3(
17870
19098
  internalCall(page, async () => {
17871
19099
  for (const button of own.buttons) await page.mouse.up({ button });
17872
19100
  for (const key of own.keys) await page.keyboard.up(key);
@@ -17880,7 +19108,7 @@ async function stopView2() {
17880
19108
  view2 = null;
17881
19109
  for (const end of viewEnds) end();
17882
19110
  viewEnds.clear();
17883
- if (shown2) await within2(shown2.close(), VIEW_END_MS2);
19111
+ if (shown2) await within3(shown2.close(), VIEW_END_MS2);
17884
19112
  }
17885
19113
  function startView2(holding) {
17886
19114
  if (view2?.stop === holding.stop) {
@@ -17900,7 +19128,7 @@ function startView2(holding) {
17900
19128
  }
17901
19129
  if (!opened) return { ok: false, message: "The page could not be shown: it closed, or does not answer." };
17902
19130
  if (held !== holding || page.isClosed()) {
17903
- await within2(opened.close(), VIEW_END_MS2);
19131
+ await within3(opened.close(), VIEW_END_MS2);
17904
19132
  return { ok: false, message: WENT_ON };
17905
19133
  }
17906
19134
  view2 = opened;
@@ -17944,7 +19172,7 @@ async function openView(page, stop) {
17944
19172
  };
17945
19173
  }
17946
19174
  async function stopViewOf(holding) {
17947
- if (viewStarting?.stop === holding.stop) await within2(viewStarting.started, VIEW_END_MS2);
19175
+ if (viewStarting?.stop === holding.stop) await within3(viewStarting.started, VIEW_END_MS2);
17948
19176
  if (view2?.stop === holding.stop) await stopView2();
17949
19177
  return { ok: true, message: "The live view is off." };
17950
19178
  }
@@ -18035,18 +19263,17 @@ async function countLocator(holding, source) {
18035
19263
  if (!locator) return { ok: false, message: `${source} is not a locator Piwi can build.`, count: null };
18036
19264
  try {
18037
19265
  const count = await internalCall(page, () => locator.count());
19266
+ let outlined = false;
18038
19267
  if (holding.bar && count > 0 && held === holding) {
18039
- const shown2 = await internalCall(page, () => locator.highlight({ style: OUTLINE_STYLE }));
18040
- const dispose = shown2?.dispose;
18041
- outline = {
18042
- stop: holding.stop,
18043
- remove: () => internalCall(page, () => typeof dispose === "function" ? dispose.call(shown2) : page.hideHighlight())
18044
- };
19268
+ const shown2 = highlightAtStop(page, locator, "count", source);
19269
+ outline = { stop: holding.stop, remove: async () => void await shown2.hide() };
19270
+ outlined = await answerWithin(shown2.shown, OUTLINE_MS, false);
18045
19271
  }
18046
19272
  return {
18047
19273
  ok: true,
18048
19274
  message: count === 0 ? "No element matches." : count === 1 ? "1 element matches." : `${count} elements match.`,
18049
- count
19275
+ count,
19276
+ outlined
18050
19277
  };
18051
19278
  } catch (error) {
18052
19279
  return { ok: false, message: `The locator could not be counted: ${errorMessage(error)}`, count: null };
@@ -18233,17 +19460,17 @@ async function imageOf(page) {
18233
19460
 
18234
19461
  // src/internal/capture/run-events.ts
18235
19462
  var begun = /* @__PURE__ */ new WeakMap();
18236
- var heard = /* @__PURE__ */ new WeakSet();
19463
+ var heard2 = /* @__PURE__ */ new WeakSet();
18237
19464
  var watched = /* @__PURE__ */ new WeakSet();
18238
- function userFile(file, cwd, windows) {
19465
+ function userFile2(file, cwd, windows) {
18239
19466
  if (!file || /[\\/]node_modules[\\/]/.test(file)) return null;
18240
- const relative10 = callSitePath(file, cwd, windows);
18241
- if (!relative10 || (windows ? path30.win32 : path30.posix).isAbsolute(relative10) || relative10.startsWith("..")) return null;
18242
- return relative10;
19467
+ const relative11 = callSitePath(file, cwd, windows);
19468
+ if (!relative11 || (windows ? path31.win32 : path31.posix).isAbsolute(relative11) || relative11.startsWith("..")) return null;
19469
+ return relative11;
18243
19470
  }
18244
19471
  function callPlace(frames, cwd = process.cwd(), windows = process.platform === "win32") {
18245
19472
  for (const frame of frames ?? []) {
18246
- const file = userFile(frame.file, cwd, windows);
19473
+ const file = userFile2(frame.file, cwd, windows);
18247
19474
  if (!file || !frame.line || frame.line < 1) continue;
18248
19475
  return { file, line: frame.line, ...frame.column && frame.column > 0 ? { column: frame.column } : {} };
18249
19476
  }
@@ -18260,9 +19487,9 @@ function errorText(error) {
18260
19487
  }
18261
19488
  function watchCalls(playwright) {
18262
19489
  const instrumentation = playwright?._instrumentation;
18263
- if (!playwright || heard.has(playwright)) return true;
19490
+ if (!playwright || heard2.has(playwright)) return true;
18264
19491
  if (!instrumentation || typeof instrumentation.addListener !== "function") return false;
18265
- heard.add(playwright);
19492
+ heard2.add(playwright);
18266
19493
  const titles = /* @__PURE__ */ new WeakMap();
18267
19494
  instrumentation.addListener({
18268
19495
  onApiCallBegin: (zone, channel) => {
@@ -18379,9 +19606,9 @@ function yamlScalar(text2) {
18379
19606
  }
18380
19607
 
18381
19608
  // src/internal/capture/record-handoff.ts
18382
- var fs26 = __toESM(require("fs"));
19609
+ var fs27 = __toESM(require("fs"));
18383
19610
  var net4 = __toESM(require("net"));
18384
- var path31 = __toESM(require("path"));
19611
+ var path32 = __toESM(require("path"));
18385
19612
  var START_WAIT_MS = 6e4;
18386
19613
  var NEW_TAB_NOTICE = "A new tab opened: what you do there is not recorded yet. Record in the first tab.";
18387
19614
  var relays = /* @__PURE__ */ new WeakMap();
@@ -18401,7 +19628,7 @@ function canRecordFromHere(channel = process.env[RECORD_CHANNEL_ENV]) {
18401
19628
  function handoffPlace(location, cwd = process.cwd()) {
18402
19629
  const m = /^(.+):(\d+):\d+$/.exec(location);
18403
19630
  if (!m) return null;
18404
- return { file: path31.resolve(cwd, m[1]), line: Number(m[2]) };
19631
+ return { file: path32.resolve(cwd, m[1]), line: Number(m[2]) };
18405
19632
  }
18406
19633
  async function recordFromHere(options) {
18407
19634
  const channel = parseRecordChannel(options.channel ?? process.env[RECORD_CHANNEL_ENV]);
@@ -18411,7 +19638,7 @@ async function recordFromHere(options) {
18411
19638
  if (ended?.aborted) return "the test ended.";
18412
19639
  const use = testInfo.project?.use ?? {};
18413
19640
  const context = page.context();
18414
- return new Promise((resolve11) => {
19641
+ return new Promise((resolve12) => {
18415
19642
  const socket2 = net4.connect(channel.path);
18416
19643
  socket2.setEncoding("utf8");
18417
19644
  const answers = /* @__PURE__ */ new Map();
@@ -18472,7 +19699,7 @@ async function recordFromHere(options) {
18472
19699
  page.off("crash", onGone);
18473
19700
  ended?.removeEventListener("abort", onEnded);
18474
19701
  socket2.end();
18475
- resolve11(recording ? null : said);
19702
+ resolve12(recording ? null : said);
18476
19703
  };
18477
19704
  const relayed = (source, payload) => new Promise((answer) => {
18478
19705
  const call = ++calls;
@@ -18504,7 +19731,7 @@ async function recordFromHere(options) {
18504
19731
  if (typeof initScript?.dispose !== "function") relay.lingering = true;
18505
19732
  }
18506
19733
  if (settled) return release();
18507
- await page.evaluate(fs26.readFileSync(bundle, "utf-8"));
19734
+ await page.evaluate(fs27.readFileSync(bundle, "utf-8"));
18508
19735
  if (settled) return release();
18509
19736
  recording = true;
18510
19737
  clearTimeout(startWait);
@@ -18564,7 +19791,7 @@ async function recordFromHere(options) {
18564
19791
 
18565
19792
  // src/internal/capture/css-coverage.ts
18566
19793
  var crypto8 = __toESM(require("crypto"));
18567
- var fs27 = __toESM(require("fs"));
19794
+ var fs28 = __toESM(require("fs"));
18568
19795
 
18569
19796
  // ../core/src/css-rules.ts
18570
19797
  var GROUPING = /* @__PURE__ */ new Set(["media", "supports", "layer", "container", "scope", "starting-style", "document"]);
@@ -18704,7 +19931,7 @@ function sourceFile(source, roots) {
18704
19931
  function styleBlockLine(file, index) {
18705
19932
  let text2;
18706
19933
  try {
18707
- text2 = fs27.readFileSync(file, "utf-8");
19934
+ text2 = fs28.readFileSync(file, "utf-8");
18708
19935
  } catch {
18709
19936
  return 0;
18710
19937
  }
@@ -18732,10 +19959,10 @@ async function sheetMap(sheet, text2, fetchMap) {
18732
19959
  return null;
18733
19960
  }
18734
19961
  }
18735
- function sameLines(text2, rules, file, lineOffset) {
19962
+ function sameLines2(text2, rules, file, lineOffset) {
18736
19963
  let source;
18737
19964
  try {
18738
- source = fs27.readFileSync(file, "utf-8");
19965
+ source = fs28.readFileSync(file, "utf-8");
18739
19966
  } catch {
18740
19967
  return false;
18741
19968
  }
@@ -18770,12 +19997,12 @@ async function resolveSheet(sheet, text2, dirs, fetchMap) {
18770
19997
  if (!file2) return null;
18771
19998
  const index = /[?&]index=(\d+)/.exec(sheet.devId);
18772
19999
  const lineOffset2 = /\.(?:vue|svelte|astro)$/i.test(file2) && index ? styleBlockLine(file2, Number(index[1])) : 0;
18773
- return { rules, lookup: null, file: file2, lineOffset: lineOffset2, sources: [], exact: sameLines(text2, rules, file2, lineOffset2) };
20000
+ return { rules, lookup: null, file: file2, lineOffset: lineOffset2, sources: [], exact: sameLines2(text2, rules, file2, lineOffset2) };
18774
20001
  }
18775
20002
  const file = fileForStyleUrl(sheet.sourceURL, dirs);
18776
20003
  if (!file) return null;
18777
20004
  const lineOffset = sheet.isInline ? sheet.startLine : 0;
18778
- return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text2, rules, file, lineOffset) };
20005
+ return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines2(text2, rules, file, lineOffset) };
18779
20006
  }
18780
20007
  function cachedResolution(sheet, text2, dirs, fetchMap) {
18781
20008
  const version = crypto8.createHash("sha1").update(text2).digest("hex");
@@ -19137,7 +20364,8 @@ function createSink() {
19137
20364
  locatorPages: new LocatorPageLog(),
19138
20365
  codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
19139
20366
  serverCoverage: /* @__PURE__ */ new Map(),
19140
- baseURL: null
20367
+ baseURL: null,
20368
+ headless: void 0
19141
20369
  };
19142
20370
  }
19143
20371
  var MAX_CODE_REACH_PAGES = 16;
@@ -19162,7 +20390,7 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
19162
20390
  async function startCodeReachOnOpenPages() {
19163
20391
  if (!pageCoverageInWorker() || OPEN_PAGES.size === 0) return;
19164
20392
  const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
19165
- await Promise.race([started, new Promise((resolve11) => setTimeout(resolve11, CODE_REACH_START_WAIT_MS).unref())]);
20393
+ await Promise.race([started, new Promise((resolve12) => setTimeout(resolve12, CODE_REACH_START_WAIT_MS).unref())]);
19166
20394
  }
19167
20395
  function configuredCodeReachRoots() {
19168
20396
  const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
@@ -19176,7 +20404,7 @@ function configuredCodeReachRoots() {
19176
20404
  }
19177
20405
  } catch {
19178
20406
  }
19179
- return raw.split(path32.delimiter).filter(Boolean);
20407
+ return raw.split(path33.delimiter).filter(Boolean);
19180
20408
  }
19181
20409
  async function stopSinkCodeReach(sink, closing) {
19182
20410
  const stopping = [];
@@ -19231,8 +20459,8 @@ async function drainPendingProbes(capMs) {
19231
20459
  let cap;
19232
20460
  await Promise.race([
19233
20461
  Promise.allSettled(PENDING_PROBES),
19234
- new Promise((resolve11) => {
19235
- cap = setTimeout(resolve11, capMs);
20462
+ new Promise((resolve12) => {
20463
+ cap = setTimeout(resolve12, capMs);
19236
20464
  })
19237
20465
  ]);
19238
20466
  clearTimeout(cap);
@@ -19291,10 +20519,10 @@ async function readWebVitals(page) {
19291
20519
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
19292
20520
  paint[key] = Math.round(entry.startTime);
19293
20521
  }
19294
- const readBuffered = (type, extra) => new Promise((resolve11) => {
20522
+ const readBuffered = (type, extra) => new Promise((resolve12) => {
19295
20523
  try {
19296
20524
  const PO = globalThis.PerformanceObserver;
19297
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve11(null);
20525
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve12(null);
19298
20526
  const out = [];
19299
20527
  const po = new PO((list) => out.push(...list.getEntries()));
19300
20528
  po.observe({ type, buffered: true, ...extra });
@@ -19304,10 +20532,10 @@ async function readWebVitals(page) {
19304
20532
  po.disconnect();
19305
20533
  } catch {
19306
20534
  }
19307
- resolve11(out);
20535
+ resolve12(out);
19308
20536
  }, 0);
19309
20537
  } catch {
19310
- resolve11(null);
20538
+ resolve12(null);
19311
20539
  }
19312
20540
  });
19313
20541
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -19424,12 +20652,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
19424
20652
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
19425
20653
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
19426
20654
  function withTimeout(promise, ms) {
19427
- return new Promise((resolve11, reject) => {
20655
+ return new Promise((resolve12, reject) => {
19428
20656
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
19429
20657
  promise.then(
19430
20658
  (value) => {
19431
20659
  clearTimeout(timer);
19432
- resolve11(value);
20660
+ resolve12(value);
19433
20661
  },
19434
20662
  (error) => {
19435
20663
  clearTimeout(timer);
@@ -19548,6 +20776,20 @@ var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
19548
20776
  var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
19549
20777
  var ACTION_METHOD_SET = new Set(ACTION_METHODS);
19550
20778
  var LOCATOR_METHOD_SET = new Set(LOCATOR_METHODS);
20779
+ var EARLY_READ_ACTIONS = /* @__PURE__ */ new Set([
20780
+ "click",
20781
+ "dblclick",
20782
+ "tap",
20783
+ "press",
20784
+ "pressSequentially",
20785
+ "type",
20786
+ "check",
20787
+ "uncheck",
20788
+ "selectOption",
20789
+ "dispatchEvent"
20790
+ ]);
20791
+ var CAPTURE_DEADLINE_MS = 500;
20792
+ var EARLY_READ_TRUSTED_MS = 250;
19551
20793
  var CAPTURED_ATTRS_ARG = {
19552
20794
  keep: [...CAPTURED_ATTRIBUTES],
19553
20795
  tagRoles: TAG_TO_ROLE,
@@ -19566,15 +20808,36 @@ var PROBE_INIT_SCRIPT = `(() => {
19566
20808
  globalThis[${JSON.stringify(PROBE_GLOBAL)}] = (el) => probe(el, arg);
19567
20809
  })();`;
19568
20810
  var PROBE_UNSEEDED_PAGES = /* @__PURE__ */ new WeakSet();
19569
- function probeElement(page, target) {
19570
- const shipSource = () => target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
20811
+ var PAGE_NAVIGATIONS = /* @__PURE__ */ new WeakMap();
20812
+ function readyProbeWorld(page) {
20813
+ if (!currentSink) return;
20814
+ try {
20815
+ const ready = internalCall(
20816
+ page,
20817
+ () => page.mainFrame().locator(":root").evaluate(() => 0, void 0, { timeout: CAPTURE_DEADLINE_MS })
20818
+ ).then(
20819
+ () => void 0,
20820
+ () => void 0
20821
+ );
20822
+ PENDING_PROBES.add(ready);
20823
+ void ready.then(() => PENDING_PROBES.delete(ready));
20824
+ } catch {
20825
+ }
20826
+ }
20827
+ function probeElement(page, target, timeout) {
20828
+ const options = timeout != null ? { timeout } : void 0;
20829
+ const shipSource = () => target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG, options);
19571
20830
  if (!page || PROBE_UNSEEDED_PAGES.has(page)) return internalCall(page, shipSource);
19572
20831
  return internalCall(
19573
20832
  page,
19574
- () => target.evaluate((el, name) => {
19575
- const seeded = globalThis[name];
19576
- return typeof seeded === "function" ? seeded(el) : null;
19577
- }, PROBE_GLOBAL).then((attrs) => {
20833
+ () => target.evaluate(
20834
+ (el, name) => {
20835
+ const seeded = globalThis[name];
20836
+ return typeof seeded === "function" ? seeded(el) : null;
20837
+ },
20838
+ PROBE_GLOBAL,
20839
+ options
20840
+ ).then((attrs) => {
19578
20841
  if (attrs) return attrs;
19579
20842
  PROBE_UNSEEDED_PAGES.add(page);
19580
20843
  return shipSource();
@@ -19609,54 +20872,60 @@ async function ariaSnapshotJSONBestEffort(target, timeout) {
19609
20872
  return null;
19610
20873
  }
19611
20874
  }
19612
- function startElementCapture(sink, page, target, seq, callerLocation, used) {
19613
- const probe = probeElement(page, target);
19614
- const settledProbe = probe.then(
19615
- () => void 0,
19616
- () => void 0
19617
- );
19618
- PENDING_PROBES.add(settledProbe);
19619
- settledProbe.then(() => PENDING_PROBES.delete(settledProbe));
19620
- sink.capturePromises.push(settledProbe);
19621
- const resolveAttrs = (async () => {
19622
- let deadline;
19623
- try {
19624
- const attrs = await Promise.race([
19625
- probe,
19626
- new Promise((_, reject) => {
19627
- deadline = setTimeout(() => reject(new Error("locator capture timeout")), 500);
19628
- })
19629
- ]);
19630
- const role = resolveAriaRole({ ...attrs, accessibleName: null });
19631
- const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
19632
- const exactName = exactAccessibleName(attrs, role);
19633
- const aria = exactName === null && (role || isFormField) ? await internalCall(page, () => ariaSnapshotBestEffort(target, 500)) : null;
19634
- const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
19635
- sink.capturedLocators[seq] = {
19636
- location: callerLocation,
19637
- used,
19638
- // hasLabel/selectorCounts inform alternative generation only —
19639
- // keep the stored element to the wire shape. rolePosition and
19640
- // ancestors ARE wire fields: the server's renamed-element match
19641
- // uses them at heal time.
19642
- element: {
19643
- tagName: attrs.tagName,
19644
- attributes: attrs.attributes,
19645
- textContent: attrs.textContent,
19646
- accessibleName,
19647
- center: attrs.center,
19648
- ...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
19649
- ...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
19650
- },
19651
- alternatives: generateAlternatives({ ...attrs, accessibleName })
19652
- };
19653
- } catch {
19654
- if (callerLocation) sink.probedLocations.delete(callerLocation);
19655
- } finally {
19656
- clearTimeout(deadline);
19657
- }
20875
+ async function readElementSnapshot(sink, page, target, callerLocation, used) {
20876
+ let deadline;
20877
+ try {
20878
+ const probe = probeElement(page, target, CAPTURE_DEADLINE_MS);
20879
+ const settledProbe = probe.then(
20880
+ () => void 0,
20881
+ () => void 0
20882
+ );
20883
+ PENDING_PROBES.add(settledProbe);
20884
+ settledProbe.then(() => PENDING_PROBES.delete(settledProbe));
20885
+ sink.capturePromises.push(settledProbe);
20886
+ const attrs = await Promise.race([
20887
+ probe,
20888
+ new Promise((_, reject) => {
20889
+ deadline = setTimeout(() => reject(new Error("locator capture timeout")), CAPTURE_DEADLINE_MS);
20890
+ })
20891
+ ]);
20892
+ const role = resolveAriaRole({ ...attrs, accessibleName: null });
20893
+ const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
20894
+ const exactName = exactAccessibleName(attrs, role);
20895
+ const aria = exactName === null && (role || isFormField) ? await internalCall(page, () => ariaSnapshotBestEffort(target, CAPTURE_DEADLINE_MS)) : null;
20896
+ const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
20897
+ return {
20898
+ location: callerLocation,
20899
+ used,
20900
+ // hasLabel/selectorCounts inform alternative generation only —
20901
+ // keep the stored element to the wire shape. rolePosition and
20902
+ // ancestors ARE wire fields: the server's renamed-element match
20903
+ // uses them at heal time.
20904
+ element: {
20905
+ tagName: attrs.tagName,
20906
+ attributes: attrs.attributes,
20907
+ textContent: attrs.textContent,
20908
+ accessibleName,
20909
+ center: attrs.center,
20910
+ ...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
20911
+ ...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
20912
+ },
20913
+ alternatives: generateAlternatives({ ...attrs, accessibleName })
20914
+ };
20915
+ } catch {
20916
+ return null;
20917
+ } finally {
20918
+ clearTimeout(deadline);
20919
+ }
20920
+ }
20921
+ function startElementCapture(sink, page, target, seq, callerLocation, used, early = null, earlyFirst = false) {
20922
+ const capture = (async () => {
20923
+ const readNow = () => readElementSnapshot(sink, page, target, callerLocation, used);
20924
+ const snapshot = earlyFirst ? await early ?? await readNow() : await readNow() ?? await early;
20925
+ if (snapshot) sink.capturedLocators[seq] = snapshot;
20926
+ else if (callerLocation) sink.probedLocations.delete(callerLocation);
19658
20927
  })();
19659
- sink.capturePromises.push(resolveAttrs);
20928
+ sink.capturePromises.push(capture);
19660
20929
  }
19661
20930
  function recordLocatorPage(sink, page, locator, location, arrival) {
19662
20931
  try {
@@ -19668,7 +20937,7 @@ function knowLocatorCall(sink, page, target, location, origin) {
19668
20937
  if (!sink?.pause || !location) return null;
19669
20938
  let settle = () => {
19670
20939
  };
19671
- const settled = new Promise((resolve11) => settle = resolve11);
20940
+ const settled = new Promise((resolve12) => settle = resolve12);
19672
20941
  const call = { page, target, location, origin, picked: false, endable: false, settled, settle };
19673
20942
  sink.locatorCalls.set(callSiteLine(location), call);
19674
20943
  return call;
@@ -19776,7 +21045,7 @@ function endedWaiting(sink, call) {
19776
21045
  return "finish";
19777
21046
  }
19778
21047
  function stopSite(sink, location) {
19779
- if (path32.dirname(path32.resolve(location.file)) !== __dirname) return stopCallSite(location);
21048
+ if (path33.dirname(path33.resolve(location.file)) !== __dirname) return stopCallSite(location);
19780
21049
  return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
19781
21050
  }
19782
21051
  function stopBarPage(sink, context, of) {
@@ -19791,7 +21060,7 @@ async function showStop(sink, context, title, location, reason, call, of, failur
19791
21060
  const page = stopBarPage(sink, context, of);
19792
21061
  const guessed = !("page" in of);
19793
21062
  if (!testInfo) return void 0;
19794
- const bar = !!page && testInfo.project?.use?.headless === false;
21063
+ const bar = !!page && pauseGateFromTestInfo(testInfo, sink.headless).headless === false;
19795
21064
  const place = runChannelOpen() ? callSitePlace(location) : null;
19796
21065
  if (!bar && !place) return void 0;
19797
21066
  const failing = failure?.failed ?? null;
@@ -19899,6 +21168,7 @@ async function showStop(sink, context, title, location, reason, call, of, failur
19899
21168
  }
19900
21169
  return "resume";
19901
21170
  }
21171
+ const stoppedOn = call?.page === page ? call : null;
19902
21172
  return internalCall(
19903
21173
  page,
19904
21174
  () => pauseHere(
@@ -19907,7 +21177,14 @@ async function showStop(sink, context, title, location, reason, call, of, failur
19907
21177
  location,
19908
21178
  call: title,
19909
21179
  attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
19910
- target: call?.page === page ? call.target : null,
21180
+ target: stoppedOn ? {
21181
+ highlight: () => highlightAtStop(
21182
+ page,
21183
+ stoppedOn.target,
21184
+ "call",
21185
+ locatorSource(stoppedOn.target, stoppedOn.origin.method, stoppedOn.origin.args)
21186
+ )
21187
+ } : null,
19911
21188
  ...failure ? { failure: true } : sink.functionEnded ? {} : { end: true }
19912
21189
  },
19913
21190
  probe,
@@ -19954,7 +21231,7 @@ function endTest(sink, call, title, location, reason) {
19954
21231
  return "end";
19955
21232
  }
19956
21233
  const said = hideSecretsInText(
19957
- `${title} at ${line} runs before the test ends: Piwi holds no locator call there. The test is skipped at its next locator call, or as it ends.`
21234
+ call && !takesEndSignal(sink.testInfo.config?.version) ? `${title} at ${line} runs before the test ends: a locator call is given up from Playwright 1.62 on. The test is skipped as the call returns.` : `${title} at ${line} runs before the test ends: Piwi holds no locator call there. The test is skipped at its next locator call, or as it ends.`
19958
21235
  );
19959
21236
  console.log(`[piwi] ${said}`);
19960
21237
  tellHub({ type: "notice", message: said });
@@ -20113,8 +21390,8 @@ function errorPlace(error, windows = process.platform === "win32") {
20113
21390
  for (const line of typeof stack === "string" ? stack.split("\n") : []) {
20114
21391
  const frame = /^\s*at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?$/.exec(line);
20115
21392
  const file = frame ? framePath(frame[1], windows) : null;
20116
- if (!frame || !file || !(windows ? path32.win32 : path32.posix).isAbsolute(file)) continue;
20117
- if (/[\\/]node_modules[\\/]/.test(file) || path32.dirname(path32.resolve(file)) === __dirname) continue;
21393
+ if (!frame || !file || !(windows ? path33.win32 : path33.posix).isAbsolute(file)) continue;
21394
+ if (/[\\/]node_modules[\\/]/.test(file) || path33.dirname(path33.resolve(file)) === __dirname) continue;
20118
21395
  return { file, line: Number(frame[2]), column: Number(frame[3]) };
20119
21396
  }
20120
21397
  return null;
@@ -20125,7 +21402,7 @@ function callInProgress(testInfo) {
20125
21402
  if (!Array.isArray(steps)) return;
20126
21403
  for (const step of steps) {
20127
21404
  if (step.endWallTime) continue;
20128
- const at = step.stack?.[0];
21405
+ const at = step.stack?.[0] ?? step.location;
20129
21406
  const called = step.category === "pw:api" || step.category === "expect" || step.category === "test.step";
20130
21407
  if (called && typeof at?.file === "string" && typeof at.line === "number") {
20131
21408
  found = {
@@ -20145,13 +21422,13 @@ function stepInProgress(testInfo) {
20145
21422
  function runningCall(testInfo) {
20146
21423
  const call = callInProgress(testInfo);
20147
21424
  if (!call?.title) return null;
20148
- const file = path32.relative(process.cwd(), call.place.file).split(path32.sep).join("/");
21425
+ const file = path33.relative(process.cwd(), call.place.file).split(path33.sep).join("/");
20149
21426
  return { title: call.title, file, line: call.place.line };
20150
21427
  }
20151
21428
  function abortion(signal) {
20152
- return new Promise((resolve11) => {
20153
- if (signal.aborted) resolve11();
20154
- else signal.addEventListener("abort", () => resolve11(), { once: true });
21429
+ return new Promise((resolve12) => {
21430
+ if (signal.aborted) resolve12();
21431
+ else signal.addEventListener("abort", () => resolve12(), { once: true });
20155
21432
  });
20156
21433
  }
20157
21434
  var END_STOP_MS = 2e3;
@@ -20186,7 +21463,7 @@ function releaseCall(sink, settled) {
20186
21463
  }
20187
21464
  function releasedRequestsEnded(sink) {
20188
21465
  if (sink.releasedRequests.size === 0) return Promise.resolve();
20189
- return new Promise((resolve11) => sink.onReleasedRequestsEnded = resolve11);
21466
+ return new Promise((resolve12) => sink.onReleasedRequestsEnded = resolve12);
20190
21467
  }
20191
21468
  async function awaitReleasedCalls(sink) {
20192
21469
  if (sink.released.length === 0) return;
@@ -20199,13 +21476,13 @@ async function awaitReleasedCalls(sink) {
20199
21476
  }
20200
21477
  const quiet = now - sink.releasedQuietSince;
20201
21478
  if (quiet >= RELEASED_QUIET_MS) return;
20202
- await new Promise((resolve11) => setTimeout(resolve11, Math.min(RELEASED_QUIET_MS - quiet, deadline - now)));
21479
+ await new Promise((resolve12) => setTimeout(resolve12, Math.min(RELEASED_QUIET_MS - quiet, deadline - now)));
20203
21480
  }
20204
21481
  }
20205
21482
  async function settledWithin(pending, ms) {
20206
21483
  if (pending.length === 0) return;
20207
21484
  let timer;
20208
- await Promise.race([Promise.allSettled(pending), new Promise((resolve11) => timer = setTimeout(resolve11, ms))]);
21485
+ await Promise.race([Promise.allSettled(pending), new Promise((resolve12) => timer = setTimeout(resolve12, ms))]);
20209
21486
  clearTimeout(timer);
20210
21487
  }
20211
21488
  function endStopsWithTestFunction(testInfo, sink) {
@@ -20238,7 +21515,7 @@ async function pauseInPlace(page, playwrightPause, location) {
20238
21515
  if (!sink || !testInfo || !location || !context || !contextDebugger(context)) {
20239
21516
  return leaveToPlaywright(context, playwrightPause);
20240
21517
  }
20241
- if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
21518
+ if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo, sink.headless)))) {
20242
21519
  return leaveToPlaywright(context, playwrightPause);
20243
21520
  }
20244
21521
  sink.pause ??= pauseState(NO_BREAKPOINTS);
@@ -20306,7 +21583,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
20306
21583
  const callerLocation = sink ? captureCallerLocation() : null;
20307
21584
  if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
20308
21585
  const call = knowLocatorCall(sink, page, target, callerLocation, { method: originMethod, args: originArgs });
20309
- const ending = sink?.pause && !sink.ended ? withEndSignal(EXPECT_METHOD, callArgs, sink.end.signal) : null;
21586
+ const ending = sink?.pause && !sink.ended && takesEndSignal(sink.testInfo?.config?.version) ? withEndSignal(EXPECT_METHOD, callArgs, sink.end.signal) : null;
20310
21587
  if (call && ending) call.endable = true;
20311
21588
  const expectArgs = ending ?? callArgs;
20312
21589
  try {
@@ -20359,7 +21636,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
20359
21636
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
20360
21637
  };
20361
21638
  const call = knowLocatorCall(sink, page, target, callerLocation, used);
20362
- const ending = sink.pause && !sink.ended ? withEndSignal(String(prop), callArgs, sink.end.signal) : null;
21639
+ const ending = sink.pause && !sink.ended && takesEndSignal(sink.testInfo?.config?.version) ? withEndSignal(String(prop), callArgs, sink.end.signal) : null;
20363
21640
  if (call && ending) call.endable = true;
20364
21641
  const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
20365
21642
  let seq = -1;
@@ -20372,7 +21649,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
20372
21649
  alternatives: []
20373
21650
  });
20374
21651
  }
21652
+ const early = !alreadyProbed && EARLY_READ_ACTIONS.has(prop) ? readElementSnapshot(sink, page, target, callerLocation, used) : null;
20375
21653
  let result;
21654
+ const startedAt = Date.now();
21655
+ const navigations = PAGE_NAVIGATIONS.get(page) ?? 0;
20376
21656
  try {
20377
21657
  result = await callMethod(target, prop, ending ?? callArgs);
20378
21658
  } catch (error) {
@@ -20385,7 +21665,8 @@ function wrapLocator(page, locator, originMethod, originArgs) {
20385
21665
  if (endsNow(sink)) throw skipOf(sink);
20386
21666
  if (!alreadyProbed && !call?.picked) {
20387
21667
  if (callerLocation) sink.probedLocations.add(callerLocation);
20388
- startElementCapture(sink, page, target, seq, callerLocation, used);
21668
+ const earlyFirst = (PAGE_NAVIGATIONS.get(page) ?? 0) !== navigations || Date.now() - startedAt < EARLY_READ_TRUSTED_MS;
21669
+ startElementCapture(sink, page, target, seq, callerLocation, used, early, earlyFirst);
20389
21670
  }
20390
21671
  return result;
20391
21672
  };
@@ -20447,7 +21728,7 @@ function instrumentPage(page) {
20447
21728
  page.close = async (...args) => {
20448
21729
  let closed = () => {
20449
21730
  };
20450
- CLOSING_PAGES.set(page, new Promise((resolve11) => closed = resolve11));
21731
+ CLOSING_PAGES.set(page, new Promise((resolve12) => closed = resolve12));
20451
21732
  try {
20452
21733
  await settleBeforeClose({ page });
20453
21734
  return await originalClose(...args);
@@ -20475,10 +21756,14 @@ function instrumentPage(page) {
20475
21756
  };
20476
21757
  }
20477
21758
  if (typeof page.on === "function") {
21759
+ page.on("domcontentloaded", () => readyProbeWorld(page));
20478
21760
  page.on("framenavigated", (frame) => {
20479
21761
  PROBE_UNSEEDED_PAGES.delete(page);
20480
21762
  try {
20481
- if (frame === page.mainFrame()) noteNavigation(page);
21763
+ if (frame === page.mainFrame()) {
21764
+ PAGE_NAVIGATIONS.set(page, (PAGE_NAVIGATIONS.get(page) ?? 0) + 1);
21765
+ noteNavigation(page);
21766
+ }
20482
21767
  } catch {
20483
21768
  }
20484
21769
  });
@@ -20759,8 +22044,8 @@ async function flushSink(sink, testInfo) {
20759
22044
  let drainDeadline;
20760
22045
  await Promise.race([
20761
22046
  Promise.allSettled(sink.capturePromises),
20762
- new Promise((resolve11) => {
20763
- drainDeadline = setTimeout(resolve11, 2e3);
22047
+ new Promise((resolve12) => {
22048
+ drainDeadline = setTimeout(resolve12, 2e3);
20764
22049
  })
20765
22050
  ]);
20766
22051
  clearTimeout(drainDeadline);
@@ -20967,7 +22252,7 @@ var runControl = {
20967
22252
  return;
20968
22253
  }
20969
22254
  if (!sink.pause) {
20970
- const why = sink.testInfo ? pauseSkipReason({ ...pauseGateFromTestInfo(sink.testInfo), channel: true }) : null;
22255
+ const why = sink.testInfo ? pauseSkipReason({ ...pauseGateFromTestInfo(sink.testInfo, sink.headless), channel: true }) : null;
20971
22256
  tellHub({ type: "notice", message: `This test does not stop${why ? `: ${why}` : ""}.` });
20972
22257
  return;
20973
22258
  }
@@ -20989,7 +22274,7 @@ var runControl = {
20989
22274
  }
20990
22275
  };
20991
22276
  function runFile(file) {
20992
- return path32.relative(process.cwd(), file).split(path32.sep).join("/") || file;
22277
+ return path33.relative(process.cwd(), file).split(path33.sep).join("/") || file;
20993
22278
  }
20994
22279
  function testBegin(testInfo, cannotPause) {
20995
22280
  const title = testInfo.titlePath.slice(1);
@@ -21029,6 +22314,7 @@ var piwiFixtures = {
21029
22314
  startResourceLedger(playwright, workerInfo.workerIndex);
21030
22315
  observeApiRequests(playwright, apiCallRecorder);
21031
22316
  openLiveView(workerInfo);
22317
+ if (liveViewOpen()) trackCalls(playwright);
21032
22318
  if (await openRunChannel(workerInfo, runControl)) watchCalls(playwright);
21033
22319
  try {
21034
22320
  await use();
@@ -21066,17 +22352,18 @@ var piwiFixtures = {
21066
22352
  // (attach the collected data) at teardown. Runs for every test without being
21067
22353
  // requested, so suites that never destructure `page` are still captured.
21068
22354
  piwiCapture: [
21069
- async ({ screenshot, baseURL }, use, testInfo) => {
22355
+ async ({ screenshot, baseURL, headless }, use, testInfo) => {
21070
22356
  boxCaptureFrames();
21071
22357
  const conflict = labModeConflict();
21072
22358
  if (conflict) throw new Error(conflict);
21073
22359
  const sink = createSink();
21074
22360
  sink.testInfo = testInfo;
21075
22361
  sink.baseURL = baseURL ?? null;
22362
+ sink.headless = headless;
21076
22363
  if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
21077
22364
  const pauseSet = currentPauseSet();
21078
22365
  const channel = runChannelOpen();
21079
- const cannotPause = pauseSkipReason({ ...pauseGateFromTestInfo(testInfo), channel });
22366
+ const cannotPause = pauseSkipReason({ ...pauseGateFromTestInfo(testInfo, headless), channel });
21080
22367
  if (pauseSet.size || channel) {
21081
22368
  if (!cannotPause) sink.pause = pauseState(pauseSet, pauseNextTest);
21082
22369
  else if (pauseSet.size) logPauseSkipOnce(cannotPause);
@@ -21108,7 +22395,7 @@ var piwiFixtures = {
21108
22395
  });
21109
22396
  activeResourceLedger()?.testStarted(testInfo);
21110
22397
  currentSink = sink;
21111
- liveViewTestBegan(testInfo.title, () => runningCall(testInfo));
22398
+ liveViewTestBegan(testInfo.title, () => runningCall(testInfo), mayPause(pauseGateFromTestInfo(testInfo)));
21112
22399
  const showRunChannel = hideRunChannel();
21113
22400
  try {
21114
22401
  await armStops(OPEN_CONTEXTS);
@@ -21126,9 +22413,10 @@ var piwiFixtures = {
21126
22413
  const applied = sink.probeInterception?.applied() ?? false;
21127
22414
  const level = sink.probeItem.level ?? "client";
21128
22415
  const handled = level === "server" && applied ? classifyProbeHandled({
21129
- consoleErrors: sink.consoleEntries.filter((e) => e.type === "error").length,
22416
+ consoleErrors: probeConsoleErrors(sink.consoleEntries),
21130
22417
  dialogs: sink.dialogs.length,
21131
22418
  backendError: sink.probeInterception?.serverError() ?? false,
22419
+ faultIsTheError: faultIsTheError(sink.probeItem.fault),
21132
22420
  pageErrors: sink.probePageErrors,
21133
22421
  blankPage: sink.probeBlankPage
21134
22422
  }) : "n/a";
@@ -21183,13 +22471,13 @@ function extendPiwiFixtures(test) {
21183
22471
  }
21184
22472
 
21185
22473
  // src/internal/ai/ai-fixtures.ts
21186
- var fs29 = __toESM(require("fs"));
21187
- var path35 = __toESM(require("path"));
22474
+ var fs30 = __toESM(require("fs"));
22475
+ var path36 = __toESM(require("path"));
21188
22476
  var import_test = require("@playwright/test");
21189
22477
 
21190
22478
  // src/internal/ai/artifact.ts
21191
- var fs28 = __toESM(require("fs"));
21192
- var path33 = __toESM(require("path"));
22479
+ var fs29 = __toESM(require("fs"));
22480
+ var path34 = __toESM(require("path"));
21193
22481
  var ARTIFACT_VERSION = 1;
21194
22482
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
21195
22483
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -21282,7 +22570,7 @@ function parseEntry(text2) {
21282
22570
  function readEntry(file) {
21283
22571
  let text2;
21284
22572
  try {
21285
- text2 = fs28.readFileSync(file, "utf8");
22573
+ text2 = fs29.readFileSync(file, "utf8");
21286
22574
  } catch (error) {
21287
22575
  if (error.code === "ENOENT") return null;
21288
22576
  throw error;
@@ -21292,28 +22580,28 @@ function readEntry(file) {
21292
22580
  function writeEntry(file, entry) {
21293
22581
  const canonical = serializeEntry(entry);
21294
22582
  return withEntryLock(file, () => {
21295
- if (fs28.existsSync(file) && fs28.readFileSync(file, "utf8") === canonical) return { written: false };
21296
- fs28.mkdirSync(path33.dirname(file), { recursive: true });
22583
+ if (fs29.existsSync(file) && fs29.readFileSync(file, "utf8") === canonical) return { written: false };
22584
+ fs29.mkdirSync(path34.dirname(file), { recursive: true });
21297
22585
  const tmp = `${file}.${process.pid}.tmp`;
21298
- fs28.writeFileSync(tmp, canonical);
21299
- fs28.renameSync(tmp, file);
22586
+ fs29.writeFileSync(tmp, canonical);
22587
+ fs29.renameSync(tmp, file);
21300
22588
  return { written: true };
21301
22589
  });
21302
22590
  }
21303
22591
  function withEntryLock(file, fn) {
21304
22592
  const lock = `${file}.lock`;
21305
- fs28.mkdirSync(path33.dirname(file), { recursive: true });
22593
+ fs29.mkdirSync(path34.dirname(file), { recursive: true });
21306
22594
  const deadline = Date.now() + 5e3;
21307
22595
  for (; ; ) {
21308
22596
  try {
21309
- const fd = fs28.openSync(lock, "wx");
21310
- fs28.closeSync(fd);
22597
+ const fd = fs29.openSync(lock, "wx");
22598
+ fs29.closeSync(fd);
21311
22599
  break;
21312
22600
  } catch (error) {
21313
22601
  if (error.code !== "EEXIST") throw error;
21314
22602
  if (Date.now() > deadline) {
21315
22603
  try {
21316
- fs28.unlinkSync(lock);
22604
+ fs29.unlinkSync(lock);
21317
22605
  } catch {
21318
22606
  }
21319
22607
  }
@@ -21323,7 +22611,7 @@ function withEntryLock(file, fn) {
21323
22611
  return fn();
21324
22612
  } finally {
21325
22613
  try {
21326
- fs28.unlinkSync(lock);
22614
+ fs29.unlinkSync(lock);
21327
22615
  } catch {
21328
22616
  }
21329
22617
  }
@@ -21331,7 +22619,7 @@ function withEntryLock(file, fn) {
21331
22619
 
21332
22620
  // src/internal/ai/keys.ts
21333
22621
  var crypto9 = __toESM(require("crypto"));
21334
- var path34 = __toESM(require("path"));
22622
+ var path35 = __toESM(require("path"));
21335
22623
  var DEFAULT_AI_DIR = "__piwi__";
21336
22624
  function normalizeTemplate(template) {
21337
22625
  return template.trim().replace(/\s+/g, " ").toLowerCase();
@@ -21345,13 +22633,13 @@ function slug(text2, maxLength = 40) {
21345
22633
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
21346
22634
  }
21347
22635
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
21348
- return path34.join(path34.dirname(specFile), dir, path34.basename(specFile));
22636
+ return path35.join(path35.dirname(specFile), dir, path35.basename(specFile));
21349
22637
  }
21350
22638
  function entryPath(params) {
21351
22639
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
21352
22640
  const hash = hashTemplate(basis, params.ordinal ?? 0);
21353
22641
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
21354
- return path34.join(entryDir(params.specFile, params.dir), name);
22642
+ return path35.join(entryDir(params.specFile, params.dir), name);
21355
22643
  }
21356
22644
  function parseLocation2(location) {
21357
22645
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -21668,9 +22956,8 @@ function compileFromCandidate(candidate) {
21668
22956
  var GROUNDED_METHODS = new Set(LOCATOR_METHODS.filter((method) => method !== "locator"));
21669
22957
  async function refLocator(page, ref) {
21670
22958
  if (!/^[a-z0-9]+$/i.test(ref)) return null;
21671
- const getByRef = page.getByRef;
21672
22959
  try {
21673
- const target = typeof getByRef === "function" ? getByRef.call(page, ref) : page.locator(`aria-ref=${ref}`);
22960
+ const target = locatorOfRef(page, ref);
21674
22961
  return await target.count() === 1 ? target : null;
21675
22962
  } catch {
21676
22963
  return null;
@@ -21954,7 +23241,7 @@ function testIdentity(testInfo) {
21954
23241
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
21955
23242
  }
21956
23243
  function relativeToCwd(file) {
21957
- return path35.relative(process.cwd(), file).split(path35.sep).join("/");
23244
+ return path36.relative(process.cwd(), file).split(path36.sep).join("/");
21958
23245
  }
21959
23246
  function recordIntents(intents, entry) {
21960
23247
  const add2 = (locator, kind) => {
@@ -21981,7 +23268,7 @@ function createAiApi(page, testInfo, config, used, intents) {
21981
23268
  const readSource = () => {
21982
23269
  if (source === null) {
21983
23270
  try {
21984
- source = fs29.readFileSync(testInfo.file, "utf8");
23271
+ source = fs30.readFileSync(testInfo.file, "utf8");
21985
23272
  } catch {
21986
23273
  source = "";
21987
23274
  }