@piwitests/reporter 0.58.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,7 +58,7 @@ __export(capture_fixtures_exports, {
58
58
  stopResourceLedger: () => stopResourceLedger
59
59
  });
60
60
  module.exports = __toCommonJS(capture_fixtures_exports);
61
- var path12 = __toESM(require("path"));
61
+ var path14 = __toESM(require("path"));
62
62
  var import_node_zlib2 = require("zlib");
63
63
 
64
64
  // ../picker-dom/src/probe.ts
@@ -412,10 +412,11 @@ function installPickerOverlay(arg) {
412
412
  }
413
413
  const Z = 2147483600;
414
414
  const testIdAttr = arg.testIdAttribute || "data-testid";
415
+ const enterReaches = arg.transport === "global";
415
416
  const words = {
416
417
  banner: "Piwi inspector \u2014 click any element to generate locators for it",
417
- keys: "\u2191 parent \xB7 \u2193 child \xB7 Esc skip",
418
- keysHovering: "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip",
418
+ keys: enterReaches ? "Enter picks the focused element \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip" : "\u2191 parent \xB7 \u2193 child \xB7 Esc skip",
419
+ keysHovering: enterReaches ? "click or Enter to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip" : "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip",
419
420
  analyzing: "Analyzing element\u2026"
420
421
  };
421
422
  for (const key of Object.keys(words)) {
@@ -510,6 +511,16 @@ function installPickerOverlay(arg) {
510
511
  const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
511
512
  return cls ? `locator(${quoteJs(`.${cls}`)})` : tag;
512
513
  };
514
+ const parentOf = (node) => {
515
+ if (node.parentElement) return node.parentElement;
516
+ const root = node.getRootNode ? node.getRootNode() : null;
517
+ return root && root !== doc && root.host ? root.host : null;
518
+ };
519
+ const targetOf = (e) => {
520
+ const path15 = typeof e.composedPath === "function" ? e.composedPath() : null;
521
+ const first = path15 && path15.length ? path15[0] : null;
522
+ return first && first.nodeType === 1 ? first : e.target;
523
+ };
513
524
  const buildChain = (raw) => {
514
525
  const chain2 = [];
515
526
  let node = raw;
@@ -517,7 +528,7 @@ function installPickerOverlay(arg) {
517
528
  const tag = (node.tagName || "").toLowerCase();
518
529
  if (tag === "body" || tag === "html") break;
519
530
  chain2.push(node);
520
- node = node.parentElement;
531
+ node = parentOf(node);
521
532
  }
522
533
  return chain2.length ? chain2 : [raw];
523
534
  };
@@ -608,10 +619,14 @@ function installPickerOverlay(arg) {
608
619
  }
609
620
  };
610
621
  const onMove = (e) => {
611
- const raw = e.target;
622
+ const raw = targetOf(e);
612
623
  if (!raw || isOwn(raw)) {
624
+ chain = [];
625
+ idx = 0;
626
+ lastRaw = null;
613
627
  highlight.style.display = "none";
614
628
  label.style.display = "none";
629
+ foot.textContent = words.keys;
615
630
  return;
616
631
  }
617
632
  if (raw !== lastRaw) {
@@ -690,17 +705,30 @@ function installPickerOverlay(arg) {
690
705
  const onClick = (e) => {
691
706
  stop(e);
692
707
  const el = current();
693
- if (!el || isOwn(e.target)) return;
708
+ if (!el || isOwn(targetOf(e))) return;
694
709
  reportPicked(el);
695
710
  };
711
+ const focused = () => {
712
+ let el = doc.activeElement;
713
+ while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement;
714
+ const tag = (el && el.tagName || "").toLowerCase();
715
+ return !el || tag === "body" || tag === "html" || isOwn(el) ? null : el;
716
+ };
696
717
  const onKey = (e) => {
697
718
  if (e.key === "Escape") {
698
719
  stop(e);
699
720
  reportSkipped();
700
721
  return;
701
722
  }
723
+ if (e.key === "Enter" && !e.isComposing) {
724
+ stop(e);
725
+ const el = current() || focused();
726
+ if (el) reportPicked(el);
727
+ return;
728
+ }
702
729
  if (e.key === "ArrowUp" || e.key === "ArrowDown") {
703
730
  stop(e);
731
+ if (!chain.length) return;
704
732
  if (e.key === "ArrowUp") idx = Math.min(idx + 1, chain.length - 1);
705
733
  else idx = Math.max(idx - 1, 0);
706
734
  refresh();
@@ -774,12 +802,43 @@ function showAnchorPicker(arg) {
774
802
  const al = n.getAttribute && n.getAttribute("aria-level");
775
803
  return al && /^\d+$/.test(al) ? Number(al) : null;
776
804
  };
805
+ const parentOf = (n) => {
806
+ if (n.parentElement) return n.parentElement;
807
+ const root = n.getRootNode ? n.getRootNode() : null;
808
+ return root && root !== doc && root.host ? root.host : null;
809
+ };
810
+ const shadowRoots = [];
811
+ const collectRoots = (root) => {
812
+ const all = root.querySelectorAll("*");
813
+ for (let i = 0; i < all.length; i++) {
814
+ const sr = all[i].shadowRoot;
815
+ if (sr) {
816
+ shadowRoots.push(sr);
817
+ collectRoots(sr);
818
+ }
819
+ }
820
+ };
821
+ try {
822
+ collectRoots(doc);
823
+ } catch {
824
+ shadowRoots.length = 0;
825
+ }
826
+ const within = (node2, scope) => {
827
+ if (scope === doc) return true;
828
+ for (let n = node2; n; n = parentOf(n)) if (n === scope) return true;
829
+ return false;
830
+ };
831
+ const queryAll = (scope, sel) => {
832
+ const found = Array.from(scope.querySelectorAll(sel));
833
+ for (const sr of shadowRoots) if (within(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
834
+ return found;
835
+ };
777
836
  const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
778
837
  const leafMatches = (scope) => {
779
838
  try {
780
- if (leafTestId) return scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(leafTestId)}]`).length;
839
+ if (leafTestId) return queryAll(scope, `[${testIdAttr}=${JSON.stringify(leafTestId)}]`).length;
781
840
  if (!leafCountable) return -1;
782
- const nodes = scope.querySelectorAll(roleSources);
841
+ const nodes = queryAll(scope, roleSources);
783
842
  if (nodes.length > 2e3) return -1;
784
843
  let matched = 0;
785
844
  for (let i = 0; i < nodes.length; i++) {
@@ -795,20 +854,20 @@ function showAnchorPicker(arg) {
795
854
  };
796
855
  let roleNodes = [];
797
856
  try {
798
- const all = doc.querySelectorAll(roleSources);
799
- if (all.length <= 4e3) roleNodes = Array.from(all);
857
+ const all = queryAll(doc, roleSources);
858
+ if (all.length <= 4e3) roleNodes = all;
800
859
  } catch {
801
860
  roleNodes = [];
802
861
  }
803
862
  const count = (sel) => {
804
863
  try {
805
- return doc.querySelectorAll(sel).length;
864
+ return queryAll(doc, sel).length;
806
865
  } catch {
807
866
  return void 0;
808
867
  }
809
868
  };
810
869
  const rows = [];
811
- let node = el.parentElement;
870
+ let node = parentOf(el);
812
871
  let depth = 0;
813
872
  while (node && depth < 12) {
814
873
  depth++;
@@ -845,7 +904,7 @@ function showAnchorPicker(arg) {
845
904
  hookLabel,
846
905
  selectable: !!(testId || id || role && (ariaLabel || info.roleCount === 1))
847
906
  });
848
- node = node.parentElement;
907
+ node = parentOf(node);
849
908
  }
850
909
  if (rows.length === 0) {
851
910
  g.__piwiAnchorState = "skipped";
@@ -874,9 +933,9 @@ function showAnchorPicker(arg) {
874
933
  footer.style.cssText = "margin:10px 0;font-weight:600;";
875
934
  const segMatches = (scope, info) => {
876
935
  try {
877
- if (info.testId) return Array.from(scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(info.testId)}]`));
878
- if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
879
- const nodes = Array.from(scope.querySelectorAll(roleSources));
936
+ if (info.testId) return queryAll(scope, `[${testIdAttr}=${JSON.stringify(info.testId)}]`);
937
+ if (info.id) return queryAll(scope, `#${doc.defaultView.CSS.escape(info.id)}`);
938
+ const nodes = queryAll(scope, roleSources);
880
939
  if (nodes.length > 2e3) return [];
881
940
  return nodes.filter(
882
941
  (n) => roleOf2(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
@@ -1163,8 +1222,11 @@ function showPauseBar(arg) {
1163
1222
  };
1164
1223
  const buttons = [
1165
1224
  ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
1166
- ["Step", "step", "Pause again at the next action", false],
1225
+ ["Step", "step", "Pause again before the next call", false],
1167
1226
  ["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
1227
+ ...arg.record ? [
1228
+ ["Record here", "record", "Record steps in this page: the editor writes them before this line", false]
1229
+ ] : [],
1168
1230
  ["Finish", "finish", "Run on and pause no more in this test", false]
1169
1231
  ];
1170
1232
  for (const [label, choice, hint, primary] of buttons) {
@@ -1926,21 +1988,21 @@ var MAX_INDETERMINATE = 20;
1926
1988
  var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
1927
1989
  var LEVEL = `${STEP}(?:/${STEP})*`;
1928
1990
  var ELEMENT_PATH_RE = new RegExp(`^${LEVEL}(?:\\|${LEVEL})*$`);
1929
- function isElementPath(path13) {
1930
- return typeof path13 === "string" && path13.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path13);
1991
+ function isElementPath(path15) {
1992
+ return typeof path15 === "string" && path15.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path15);
1931
1993
  }
1932
1994
  var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
1933
1995
  function elementRef(v) {
1934
1996
  if (!v || typeof v !== "object") return null;
1935
- const { path: path13, label } = v;
1936
- if (!isElementPath(path13)) return null;
1937
- return { path: path13, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
1997
+ const { path: path15, label } = v;
1998
+ if (!isElementPath(path15)) return null;
1999
+ return { path: path15, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
1938
2000
  }
1939
2001
  function textPoint(v) {
1940
2002
  if (!v || typeof v !== "object") return null;
1941
- const { path: path13, offset } = v;
2003
+ const { path: path15, offset } = v;
1942
2004
  const at = offsetOf(offset);
1943
- return isElementPath(path13) && at !== null ? { path: path13, offset: at } : null;
2005
+ return isElementPath(path15) && at !== null ? { path: path15, offset: at } : null;
1944
2006
  }
1945
2007
  function sanitizePageInteraction(raw) {
1946
2008
  if (!raw || typeof raw !== "object") return {};
@@ -2346,7 +2408,8 @@ var ATTACHMENT_NAMES = {
2346
2408
  userPick: "piwi-user-pick",
2347
2409
  aiUsage: "piwi-ai-usage",
2348
2410
  aiMeta: "piwi-ai-meta",
2349
- resources: "piwi-resources"
2411
+ resources: "piwi-resources",
2412
+ pauses: "piwi-pauses"
2350
2413
  };
2351
2414
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
2352
2415
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
@@ -2580,13 +2643,19 @@ var LocatorPageLog = class {
2580
2643
  };
2581
2644
 
2582
2645
  // src/internal/probe/mode.ts
2583
- var fs2 = __toESM(require("fs"));
2646
+ var fs3 = __toESM(require("fs"));
2647
+
2648
+ // src/internal/config/env.ts
2649
+ var path3 = __toESM(require("path"));
2584
2650
 
2585
2651
  // src/internal/config/desktop.ts
2586
2652
  var fs = __toESM(require("fs"));
2587
2653
  var os = __toESM(require("os"));
2588
2654
  var path2 = __toESM(require("path"));
2589
2655
 
2656
+ // src/internal/config/dotenv.ts
2657
+ var fs2 = __toESM(require("fs"));
2658
+
2590
2659
  // src/internal/config/env.ts
2591
2660
  var DEFAULTS = {
2592
2661
  projectName: "default-project",
@@ -2623,6 +2692,7 @@ var DEFAULTS = {
2623
2692
  };
2624
2693
  var PIWI_ENV_KEYS = {
2625
2694
  serverUrl: "PIWI_DASHBOARD_URL",
2695
+ desktopApp: "PIWI_DESKTOP_APP",
2626
2696
  projectName: "PIWI_PROJECT_NAME",
2627
2697
  verbose: "PIWI_VERBOSE",
2628
2698
  apiKey: "PIWI_API_KEY",
@@ -2720,14 +2790,14 @@ var ENV_FALLBACK_SPECS = [
2720
2790
  ];
2721
2791
 
2722
2792
  // src/internal/support/test-paths.ts
2723
- var path3 = __toESM(require("path"));
2793
+ var path4 = __toESM(require("path"));
2724
2794
  function configDirFromConfig(config) {
2725
- if (config?.configFile) return path3.dirname(config.configFile);
2795
+ if (config?.configFile) return path4.dirname(config.configFile);
2726
2796
  if (config?.rootDir) return config.rootDir;
2727
2797
  return process.cwd();
2728
2798
  }
2729
2799
  function relativeTestPath(file, base) {
2730
- return path3.relative(base, file).split(path3.sep).join("/");
2800
+ return path4.relative(base, file).split(path4.sep).join("/");
2731
2801
  }
2732
2802
 
2733
2803
  // src/internal/probe/plan.ts
@@ -2737,8 +2807,8 @@ function parseRouteKey(routeKey2) {
2737
2807
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
2738
2808
  }
2739
2809
  function patternToRegex(pattern) {
2740
- const path13 = pattern.split("?")[0] ?? pattern;
2741
- const escaped = path13.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2810
+ const path15 = pattern.split("?")[0] ?? pattern;
2811
+ const escaped = path15.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2742
2812
  return new RegExp(`^${escaped}$`);
2743
2813
  }
2744
2814
  function suitePathEqual(a, b) {
@@ -2761,13 +2831,13 @@ function matchProbeItem(plan, test) {
2761
2831
  function requestMatchesRoute(method, url, routeKey2) {
2762
2832
  const { method: rm, pattern } = parseRouteKey(routeKey2);
2763
2833
  if (rm && rm !== method.toUpperCase()) return false;
2764
- let path13;
2834
+ let path15;
2765
2835
  try {
2766
- path13 = new URL(url).pathname;
2836
+ path15 = new URL(url).pathname;
2767
2837
  } catch {
2768
- path13 = url.split("?")[0] ?? url;
2838
+ path15 = url.split("?")[0] ?? url;
2769
2839
  }
2770
- return patternToRegex(pattern).test(path13);
2840
+ return patternToRegex(pattern).test(path15);
2771
2841
  }
2772
2842
  function createProbeState() {
2773
2843
  return { navigated: false, matches: 0 };
@@ -2789,10 +2859,10 @@ function isProbeMode() {
2789
2859
  function loadPlan() {
2790
2860
  if (planCache !== void 0) return planCache;
2791
2861
  planCache = null;
2792
- const path13 = process.env[PIWI_PROBE_ENV.plan];
2793
- if (path13) {
2862
+ const path15 = process.env[PIWI_PROBE_ENV.plan];
2863
+ if (path15) {
2794
2864
  try {
2795
- planCache = JSON.parse(fs2.readFileSync(path13, "utf8"));
2865
+ planCache = JSON.parse(fs3.readFileSync(path15, "utf8"));
2796
2866
  } catch {
2797
2867
  planCache = null;
2798
2868
  }
@@ -2816,10 +2886,10 @@ function outcomeFromStatus(status, applied) {
2816
2886
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2817
2887
  }
2818
2888
  function recordProbeOutcome(line) {
2819
- const path13 = process.env[PIWI_PROBE_ENV.results];
2820
- if (!path13) return;
2889
+ const path15 = process.env[PIWI_PROBE_ENV.results];
2890
+ if (!path15) return;
2821
2891
  try {
2822
- fs2.appendFileSync(path13, `${JSON.stringify(line)}
2892
+ fs3.appendFileSync(path15, `${JSON.stringify(line)}
2823
2893
  `);
2824
2894
  } catch {
2825
2895
  }
@@ -2949,7 +3019,7 @@ function originOf(url) {
2949
3019
  return null;
2950
3020
  }
2951
3021
  }
2952
- var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, Math.max(0, ms)));
3022
+ var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, Math.max(0, ms)));
2953
3023
  async function performRouteAction(route, action, startedAt) {
2954
3024
  switch (action.kind) {
2955
3025
  case "abort":
@@ -3060,7 +3130,7 @@ async function installProbeInterception(page, item) {
3060
3130
  }
3061
3131
 
3062
3132
  // src/internal/flake/mode.ts
3063
- var fs3 = __toESM(require("fs"));
3133
+ var fs4 = __toESM(require("fs"));
3064
3134
 
3065
3135
  // ../core/src/error-parse.ts
3066
3136
  var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
@@ -3371,7 +3441,7 @@ function loadFlakePlan() {
3371
3441
  if (planCache2 && planCache2.file === file) return planCache2.plan;
3372
3442
  let text;
3373
3443
  try {
3374
- text = fs3.readFileSync(file, "utf8");
3444
+ text = fs4.readFileSync(file, "utf8");
3375
3445
  } catch (error) {
3376
3446
  throw new Error(`[Piwi Dashboard] Cannot read the flake plan ${file}: ${errorMessage(error)}`);
3377
3447
  }
@@ -3524,7 +3594,7 @@ function recordFlakeResult(line) {
3524
3594
  const file = process.env[PIWI_FLAKE_ENV.results]?.trim();
3525
3595
  if (!file) return;
3526
3596
  try {
3527
- fs3.appendFileSync(file, `${JSON.stringify(line)}
3597
+ fs4.appendFileSync(file, `${JSON.stringify(line)}
3528
3598
  `);
3529
3599
  } catch {
3530
3600
  }
@@ -3590,13 +3660,13 @@ function normalizeFunctionRanges(ranges) {
3590
3660
  }
3591
3661
  function normalizeCoveragePath(raw) {
3592
3662
  if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
3593
- const path13 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
3594
- if (!path13 || path13.startsWith("../") || path13.startsWith("/") || /^[A-Za-z]:\//.test(path13)) return null;
3595
- if (/(^|\/)node_modules\//.test(path13)) return null;
3596
- return path13;
3663
+ const path15 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
3664
+ if (!path15 || path15.startsWith("../") || path15.startsWith("/") || /^[A-Za-z]:\//.test(path15)) return null;
3665
+ if (/(^|\/)node_modules\//.test(path15)) return null;
3666
+ return path15;
3597
3667
  }
3598
- function fileEntry(path13, runtime, blob, functions = [], lines = []) {
3599
- const file = { path: path13, runtime };
3668
+ function fileEntry(path15, runtime, blob, functions = [], lines = []) {
3669
+ const file = { path: path15, runtime };
3600
3670
  if (blob && (functions.length > 0 || lines.length > 0)) file.blob = blob;
3601
3671
  if (functions.length > 0) file.functions = functions;
3602
3672
  if (lines.length > 0) file.lines = lines;
@@ -3615,11 +3685,11 @@ function sanitizeCoverage(raw) {
3615
3685
  const files = [];
3616
3686
  for (const item of raw.files) {
3617
3687
  if (!isRecord(item) || !isRuntime(item.runtime)) continue;
3618
- const path13 = normalizeCoveragePath(item.path);
3619
- if (path13 === null) continue;
3688
+ const path15 = normalizeCoveragePath(item.path);
3689
+ if (path15 === null) continue;
3620
3690
  const blob = typeof item.blob === "string" && BLOB_ID.test(item.blob) ? item.blob : void 0;
3621
3691
  files.push(
3622
- fileEntry(path13, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
3692
+ fileEntry(path15, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
3623
3693
  );
3624
3694
  }
3625
3695
  const merged = mergeCoverage(captureOf(files, raw.incomplete === true));
@@ -4030,7 +4100,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
4030
4100
  }
4031
4101
 
4032
4102
  // src/internal/capture/pick-on-failure.ts
4033
- var path4 = __toESM(require("path"));
4103
+ var path5 = __toESM(require("path"));
4034
4104
  function parseLeafLocatorExpression(rawExpr) {
4035
4105
  const leaf = parseLeafLocatorCall(rawExpr);
4036
4106
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -4045,37 +4115,60 @@ ${err.stack ?? ""}`));
4045
4115
  const parsed2 = parseLeafLocatorExpression(header);
4046
4116
  if (!parsed2) continue;
4047
4117
  const loc = err.location;
4048
- const location = loc ? `${path4.relative(process.cwd(), loc.file).split(path4.sep).join("/")}:${loc.line}:${loc.column}` : null;
4118
+ const location = loc ? `${path5.relative(process.cwd(), loc.file).split(path5.sep).join("/")}:${loc.line}:${loc.column}` : null;
4049
4119
  return { method: parsed2.method, args: parsed2.args, location };
4050
4120
  }
4051
4121
  return null;
4052
4122
  }
4053
- async function cleanupPicker(page) {
4123
+ var PICK_ENDED = "__piwiPickEnded";
4124
+ async function cleanupPicker(page, ended = false) {
4054
4125
  try {
4055
- await page.evaluate(() => {
4056
- const g = globalThis;
4057
- if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
4058
- if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
4059
- delete g.__piwiPickCleanup;
4060
- delete g.__piwiAnchorCleanup;
4061
- delete g.__piwiPickState;
4062
- delete g.__piwiPickChoice;
4063
- delete g.__piwiPickedElement;
4064
- delete g.__piwiAnchorState;
4065
- delete g.__piwiPickAnchors;
4066
- delete g.__piwiPickChainCount;
4067
- });
4126
+ await page.evaluate(
4127
+ ([endedKey, unanswered]) => {
4128
+ const g = globalThis;
4129
+ if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
4130
+ if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
4131
+ delete g.__piwiPickCleanup;
4132
+ delete g.__piwiAnchorCleanup;
4133
+ delete g.__piwiPickState;
4134
+ delete g.__piwiPickChoice;
4135
+ delete g.__piwiPickedElement;
4136
+ delete g.__piwiAnchorState;
4137
+ delete g.__piwiPickAnchors;
4138
+ delete g.__piwiPickChainCount;
4139
+ if (unanswered) g[endedKey] = true;
4140
+ else delete g[endedKey];
4141
+ },
4142
+ [PICK_ENDED, ended]
4143
+ );
4068
4144
  } catch {
4069
4145
  }
4070
4146
  }
4071
- async function pickLocator(page, probe, failing) {
4147
+ function unlessEnded(run, ended) {
4148
+ if (!ended) return run;
4149
+ return Promise.race([
4150
+ run,
4151
+ new Promise((resolve5) => {
4152
+ if (ended.aborted) resolve5(void 0);
4153
+ else ended.addEventListener("abort", () => resolve5(void 0), { once: true });
4154
+ })
4155
+ ]);
4156
+ }
4157
+ async function pickLocator(page, probe, failing, ended) {
4158
+ const answered = async (name) => {
4159
+ const wait = page.waitForFunction(
4160
+ ([key, endedKey]) => globalThis[key] !== void 0 || globalThis[endedKey] === true,
4161
+ [name, PICK_ENDED],
4162
+ { timeout: 0, polling: 250 }
4163
+ );
4164
+ await unlessEnded(wait, ended);
4165
+ return !ended?.aborted;
4166
+ };
4072
4167
  try {
4073
4168
  const overlayArg = { transport: "global", failing };
4169
+ await page.evaluate((endedKey) => delete globalThis[endedKey], PICK_ENDED);
4074
4170
  await page.evaluate(installPickerOverlay, overlayArg);
4075
- await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
4076
- timeout: 0,
4077
- polling: 250
4078
- });
4171
+ if (!await answered("__piwiPickState")) return null;
4079
4172
  const state = await page.evaluate(() => globalThis.__piwiPickState);
4080
4173
  if (state !== "picked") return null;
4081
4174
  const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
@@ -4096,10 +4189,7 @@ async function pickLocator(page, probe, failing) {
4096
4189
  leafLevel: level,
4097
4190
  leafTestId: attrs.attributes["data-testid"] ?? null
4098
4191
  });
4099
- await page.waitForFunction(() => globalThis.__piwiAnchorState !== void 0, void 0, {
4100
- timeout: 0,
4101
- polling: 250
4102
- });
4192
+ if (!await answered("__piwiAnchorState")) return null;
4103
4193
  const anchorResult = await page.evaluate(() => {
4104
4194
  const g = globalThis;
4105
4195
  return { state: g.__piwiAnchorState, anchors: g.__piwiPickAnchors ?? [], chainCount: g.__piwiPickChainCount };
@@ -4121,10 +4211,7 @@ async function pickLocator(page, probe, failing) {
4121
4211
  failing,
4122
4212
  choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
4123
4213
  });
4124
- await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
4125
- timeout: 0,
4126
- polling: 250
4127
- });
4214
+ if (!await answered("__piwiPickChoice")) return null;
4128
4215
  const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
4129
4216
  if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
4130
4217
  const picked = { ...ranked[choice], pickedByUser: true };
@@ -4143,7 +4230,7 @@ async function pickLocator(page, probe, failing) {
4143
4230
  ...anchors.length > 0 ? { anchors } : {}
4144
4231
  };
4145
4232
  } finally {
4146
- await cleanupPicker(page);
4233
+ await cleanupPicker(page, ended?.aborted);
4147
4234
  }
4148
4235
  }
4149
4236
  async function runLocatorPicker(page, testInfo, failed, probe) {
@@ -4198,7 +4285,7 @@ function applyPickToSnapshots(snapshots, pick) {
4198
4285
  }
4199
4286
 
4200
4287
  // src/internal/capture/pause-at.ts
4201
- var path5 = __toESM(require("path"));
4288
+ var path8 = __toESM(require("path"));
4202
4289
 
4203
4290
  // ../core/src/editor-send.ts
4204
4291
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
@@ -4224,6 +4311,93 @@ function isInsidePath(file) {
4224
4311
  return file.split(/[\\/]/).every((part) => part !== "..");
4225
4312
  }
4226
4313
 
4314
+ // src/internal/capture/quiet-capture.ts
4315
+ var import_node_module = require("module");
4316
+ var path7 = __toESM(require("path"));
4317
+
4318
+ // src/internal/support/reporter-version.ts
4319
+ var fs5 = __toESM(require("fs"));
4320
+ var path6 = __toESM(require("path"));
4321
+ var CANDIDATE_OFFSETS = ["..", "../../.."];
4322
+ function findOwnPackageJson(fromDir) {
4323
+ for (const offset of CANDIDATE_OFFSETS) {
4324
+ try {
4325
+ const pkgPath = path6.resolve(fromDir, offset, "package.json");
4326
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
4327
+ const { name, version } = pkg;
4328
+ if (name === "@piwitests/reporter" && typeof version === "string")
4329
+ return { version, root: path6.dirname(pkgPath) };
4330
+ } catch {
4331
+ }
4332
+ }
4333
+ return void 0;
4334
+ }
4335
+
4336
+ // src/internal/capture/quiet-capture.ts
4337
+ var BOXED_PREFIX_EXPORTS = [
4338
+ [
4339
+ "playwright-core/lib/coreBundle",
4340
+ (mod) => mod?.utils?.setBoxedStackPrefixes
4341
+ ],
4342
+ ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
4343
+ ];
4344
+ var boxing;
4345
+ var resolvedPlaywrightPackageJson;
4346
+ function resolvePlaywrightPackageJson() {
4347
+ if (resolvedPlaywrightPackageJson !== void 0) return resolvedPlaywrightPackageJson;
4348
+ try {
4349
+ const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
4350
+ resolvedPlaywrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
4351
+ } catch {
4352
+ resolvedPlaywrightPackageJson = null;
4353
+ }
4354
+ return resolvedPlaywrightPackageJson;
4355
+ }
4356
+ function resolveBoxing() {
4357
+ try {
4358
+ const root = findOwnPackageJson(__dirname)?.root;
4359
+ if (!root) return null;
4360
+ const playwrightPackageJson = resolvePlaywrightPackageJson();
4361
+ if (!playwrightPackageJson) return null;
4362
+ const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
4363
+ for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
4364
+ let set;
4365
+ try {
4366
+ set = pick(requireFromPlaywright(moduleId));
4367
+ } catch {
4368
+ continue;
4369
+ }
4370
+ if (typeof set !== "function") continue;
4371
+ return {
4372
+ set,
4373
+ prefixes: [path7.dirname(playwrightPackageJson), path7.join(root, "dist") + path7.sep]
4374
+ };
4375
+ }
4376
+ } catch {
4377
+ }
4378
+ return null;
4379
+ }
4380
+ function boxCaptureFrames(extraPrefixes = []) {
4381
+ const first = boxing === void 0;
4382
+ if (first) boxing = resolveBoxing();
4383
+ if (!boxing) return;
4384
+ const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
4385
+ if (!first && added.length === 0) return;
4386
+ boxing.prefixes.push(...added);
4387
+ try {
4388
+ boxing.set([...boxing.prefixes]);
4389
+ } catch {
4390
+ }
4391
+ }
4392
+ function callMethod(target, prop, args) {
4393
+ return target[prop](...args);
4394
+ }
4395
+ function internalCall(owner, run) {
4396
+ const wrap = owner?._wrapApiCall;
4397
+ if (typeof wrap !== "function") return run();
4398
+ return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
4399
+ }
4400
+
4227
4401
  // src/internal/capture/pause-at.ts
4228
4402
  var PauseSet = class {
4229
4403
  constructor(lines) {
@@ -4239,18 +4413,32 @@ var PauseSet = class {
4239
4413
  return col > 0 && this.lines.has(callerLocation.slice(0, col));
4240
4414
  }
4241
4415
  };
4242
- var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
4416
+ var NO_BREAKPOINTS = new PauseSet(/* @__PURE__ */ new Set());
4243
4417
  function callSiteFile(file, cwd) {
4244
4418
  const slashed = file.replace(/\\/g, "/");
4245
- const absolute = path5.isAbsolute(file) || path5.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
4246
- let rel = absolute ? path5.relative(cwd, file) : path5.normalize(slashed);
4247
- rel = rel.split(path5.sep).join("/").replace(/\\/g, "/");
4419
+ const absolute = path8.isAbsolute(file) || path8.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
4420
+ let rel = absolute ? path8.relative(cwd, file) : path8.normalize(slashed);
4421
+ rel = rel.split(path8.sep).join("/").replace(/\\/g, "/");
4248
4422
  if (rel.startsWith("./")) rel = rel.slice(2);
4249
4423
  return rel;
4250
4424
  }
4425
+ function stopCallSite(location, cwd = process.cwd()) {
4426
+ if (!location.line) return null;
4427
+ return `${callSiteFile(location.file, cwd)}:${location.line}:${location.column ?? 0}`;
4428
+ }
4429
+ function callSiteLine(callSite) {
4430
+ const col = callSite.lastIndexOf(":");
4431
+ return col > 0 ? callSite.slice(0, col) : callSite;
4432
+ }
4433
+ function inFlightCallSite(inFlight) {
4434
+ const [first] = inFlight;
4435
+ if (first === void 0) return null;
4436
+ const line = callSiteLine(first);
4437
+ return inFlight.every((site) => callSiteLine(site) === line) ? first : null;
4438
+ }
4251
4439
  var ignoredLogged = /* @__PURE__ */ new Set();
4252
4440
  function parsePauseAt(value, cwd = process.cwd()) {
4253
- if (!value?.trim()) return EMPTY;
4441
+ if (!value?.trim()) return NO_BREAKPOINTS;
4254
4442
  const lines = /* @__PURE__ */ new Set();
4255
4443
  for (const raw of value.split(";")) {
4256
4444
  const entry = raw.trim();
@@ -4266,7 +4454,7 @@ function parsePauseAt(value, cwd = process.cwd()) {
4266
4454
  }
4267
4455
  lines.add(`${callSiteFile(m[1], cwd)}:${line}`);
4268
4456
  }
4269
- return lines.size ? new PauseSet(lines) : EMPTY;
4457
+ return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
4270
4458
  }
4271
4459
  var parsed = null;
4272
4460
  function currentPauseSet() {
@@ -4276,27 +4464,121 @@ function currentPauseSet() {
4276
4464
  }
4277
4465
  function pauseGateFromTestInfo(testInfo) {
4278
4466
  const use = testInfo.project?.use ?? {};
4279
- return { ci: process.env.CI, headless: use.headless };
4467
+ const debug = testInfo._configInternal?.configCLIOverrides?.debug;
4468
+ return { ci: process.env.CI, headless: use.headless, pwdebug: process.env.PWDEBUG, debug };
4280
4469
  }
4281
4470
  function pauseSkipReason(gate) {
4282
4471
  if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
4283
4472
  if (gate.headless !== false)
4284
4473
  return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
4474
+ if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug))
4475
+ return "Playwright's Inspector runs the test (PWDEBUG) \u2014 run without --debug";
4476
+ if (typeof gate.debug === "string" && gate.debug)
4477
+ return `Playwright's debugger runs the test (--debug=${gate.debug}) \u2014 run without --debug`;
4285
4478
  return null;
4286
4479
  }
4287
- function rearmedTimeout(before, pausedMs) {
4288
- return before > 0 ? before + Math.max(0, Math.round(pausedMs)) : 0;
4480
+ function atStop(state, callSite) {
4481
+ if (callSite && !state.finished) {
4482
+ if (state.step) {
4483
+ state.step = false;
4484
+ return { show: "step" };
4485
+ }
4486
+ if (state.set.matches(callSite)) return { show: "breakpoint" };
4487
+ }
4488
+ return { go: goOn(state) };
4489
+ }
4490
+ function goOn(state, answer) {
4491
+ if (answer === "step") state.step = true;
4492
+ else if (answer === "finish") state.finished = true;
4493
+ return !state.finished && (state.step || state.set.size > 0) ? "next" : "resume";
4494
+ }
4495
+ function contextDebugger(context) {
4496
+ const dbg = context.debugger;
4497
+ return typeof dbg?.requestPause === "function" && typeof dbg.on === "function" ? dbg : null;
4498
+ }
4499
+ var WATCHED = /* @__PURE__ */ new WeakSet();
4500
+ function watchStops(context, onStop) {
4501
+ const dbg = contextDebugger(context);
4502
+ if (!dbg) return false;
4503
+ if (WATCHED.has(context)) return true;
4504
+ WATCHED.add(context);
4505
+ inspectorSwitch.watching = true;
4506
+ syncInspectorSwitch();
4507
+ dbg.on("pausedstatechanged", () => onStop(context));
4508
+ return true;
4509
+ }
4510
+ var NO_INSPECTOR_ENV = "PW_CODEGEN_NO_INSPECTOR";
4511
+ var inspectorSwitch = {
4512
+ /** Set by Piwi, so Piwi's to clear. */
4513
+ set: false,
4514
+ /** Piwi answers the stops of a context in this worker. */
4515
+ watching: false,
4516
+ /** Every `page.pause()` of the worker's Playwright reaches it through Piwi (`markPausesWrapped`). */
4517
+ wrapped: false,
4518
+ /** The `page.pause()` calls left to Playwright that are running (`withInspector`). */
4519
+ open: 0
4520
+ };
4521
+ function syncInspectorSwitch() {
4522
+ const closed = inspectorSwitch.watching && inspectorSwitch.wrapped && inspectorSwitch.open === 0;
4523
+ if (closed && process.env[NO_INSPECTOR_ENV] === void 0) {
4524
+ process.env[NO_INSPECTOR_ENV] = "1";
4525
+ inspectorSwitch.set = true;
4526
+ } else if (!closed && inspectorSwitch.set) {
4527
+ if (process.env[NO_INSPECTOR_ENV] === "1") delete process.env[NO_INSPECTOR_ENV];
4528
+ inspectorSwitch.set = false;
4529
+ }
4530
+ }
4531
+ function markPausesWrapped() {
4532
+ inspectorSwitch.wrapped = true;
4533
+ syncInspectorSwitch();
4534
+ }
4535
+ async function withInspector(run) {
4536
+ inspectorSwitch.open++;
4537
+ syncInspectorSwitch();
4538
+ try {
4539
+ return await run();
4540
+ } finally {
4541
+ inspectorSwitch.open--;
4542
+ syncInspectorSwitch();
4543
+ }
4544
+ }
4545
+ async function steerDebugger(context, how) {
4546
+ const dbg = contextDebugger(context);
4547
+ if (!dbg) return;
4548
+ try {
4549
+ await internalCall(
4550
+ context,
4551
+ () => how === "pause" ? dbg.requestPause() : how === "next" ? dbg.next() : dbg.resume()
4552
+ );
4553
+ } catch {
4554
+ }
4555
+ }
4556
+ async function withTestTimeoutHeld(testInfo, run) {
4557
+ const set = testInfo._setIgnoreTimeouts;
4558
+ if (typeof set !== "function") return run();
4559
+ set.call(testInfo, true);
4560
+ try {
4561
+ return await run();
4562
+ } finally {
4563
+ set.call(testInfo, false);
4564
+ }
4565
+ }
4566
+ function testTimeSlot(testInfo) {
4567
+ try {
4568
+ const slot = testInfo._timeoutManager?.defaultSlot?.();
4569
+ return slot && typeof slot.elapsed === "number" ? slot : null;
4570
+ } catch {
4571
+ return null;
4572
+ }
4573
+ }
4574
+ function restoreTestTime(testInfo, heldAt) {
4575
+ const slot = testTimeSlot(testInfo);
4576
+ if (slot && Number.isFinite(heldAt) && heldAt >= 0 && heldAt < slot.elapsed) slot.elapsed = heldAt;
4289
4577
  }
4290
4578
  function pausePlace(callerLocation) {
4291
- const col = callerLocation.lastIndexOf(":");
4292
- const fileLine = col > 0 ? callerLocation.slice(0, col) : callerLocation;
4579
+ const fileLine = callSiteLine(callerLocation);
4293
4580
  return fileLine.slice(fileLine.lastIndexOf("/") + 1);
4294
4581
  }
4295
- function assertionLabel(expression, isNot) {
4296
- const [first = "", ...rest] = expression.split(".").filter(Boolean);
4297
- const name = first + rest.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
4298
- return `${isNot ? "not." : ""}${name || "expect"}`;
4299
- }
4300
4582
  function pickPlace(callerLocation) {
4301
4583
  const m = /^(.+):(\d+):\d+$/.exec(callerLocation);
4302
4584
  if (!m) return null;
@@ -4341,59 +4623,312 @@ function logPauseSkipOnce(reason) {
4341
4623
  skipLogged = true;
4342
4624
  console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt} is set but breakpoints are skipped: ${reason}`);
4343
4625
  }
4626
+ var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
4627
+ var MAX_NAVIGATED = 10;
4344
4628
  var HIGHLIGHT_MS = 2e3;
4345
- async function pauseHere(page, testInfo, point, probe, onPick) {
4346
- const before = testInfo.timeout;
4347
- const started = Date.now();
4629
+ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
4348
4630
  let shown = null;
4349
4631
  try {
4350
- testInfo.setTimeout(0);
4351
4632
  const highlight = point.target?.highlight;
4352
4633
  if (typeof highlight === "function") {
4353
4634
  let timer;
4354
4635
  shown = await Promise.race([
4355
4636
  Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
4356
- new Promise((resolve3) => {
4357
- timer = setTimeout(() => resolve3(null), HIGHLIGHT_MS);
4637
+ new Promise((resolve5) => {
4638
+ timer = setTimeout(() => resolve5(null), HIGHLIGHT_MS);
4358
4639
  })
4359
4640
  ]);
4360
4641
  clearTimeout(timer);
4361
4642
  }
4362
4643
  const arg = {
4363
4644
  place: pausePlace(point.location),
4364
- attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
4365
- action: point.action,
4366
- locator: point.locator
4645
+ attempt: point.attempt,
4646
+ action: point.call,
4647
+ locator: null,
4648
+ record: !!onRecord
4367
4649
  };
4368
4650
  console.log(
4369
- `[piwi] Paused at ${point.location} before ${point.locator ? `${point.action} \xB7 ${point.locator}` : point.action}: Resume, Step, Pick a locator or Finish in the browser (Esc resumes).`
4651
+ `[piwi] Paused at ${callSiteLine(point.location)} before ${point.call}: Resume, Step, Pick a locator${onRecord ? ", Record here" : ""} or Finish in the browser (Esc resumes).`
4370
4652
  );
4653
+ let navigated = 0;
4371
4654
  for (; ; ) {
4372
- await page.evaluate(showPauseBar, arg);
4373
- await page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 });
4374
- const choice = await page.evaluate(takePauseState);
4375
- if (choice === "resume" || choice === "step" || choice === "finish") return choice;
4376
- if (choice !== "pick") continue;
4377
- const picked = await pickLocator(page, probe, null).catch(() => null);
4378
- if (picked) await onPick(picked);
4655
+ try {
4656
+ if (ended?.aborted) return "resume";
4657
+ await page.evaluate(showPauseBar, arg);
4658
+ await unlessEnded(page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 }), ended);
4659
+ if (ended?.aborted) return "resume";
4660
+ const choice = await page.evaluate(takePauseState);
4661
+ navigated = 0;
4662
+ if (choice === "resume" || choice === "step" || choice === "finish") return choice;
4663
+ if (choice === "record" && onRecord) {
4664
+ await unlessEnded(
4665
+ onRecord(ended).catch(() => void 0),
4666
+ ended
4667
+ );
4668
+ continue;
4669
+ }
4670
+ if (choice !== "pick") continue;
4671
+ const picked = await pickLocator(page, probe, null, ended).catch(() => null);
4672
+ if (picked && !ended?.aborted) await onPick(picked);
4673
+ } catch (error) {
4674
+ if (page.isClosed() || !NAVIGATED.test(String(error)) || ++navigated > MAX_NAVIGATED) throw error;
4675
+ await page.waitForLoadState("domcontentloaded").catch(() => void 0);
4676
+ }
4379
4677
  }
4380
4678
  } catch {
4381
4679
  return "resume";
4382
4680
  } finally {
4383
4681
  try {
4682
+ if (ended?.aborted) await page.evaluate(takePauseState);
4384
4683
  const dispose = shown?.dispose;
4385
4684
  const hide = page.hideHighlight;
4386
4685
  if (typeof dispose === "function") await dispose.call(shown);
4387
4686
  else if (typeof hide === "function") await hide.call(page);
4388
4687
  } catch {
4389
4688
  }
4390
- try {
4391
- testInfo.setTimeout(rearmedTimeout(before, Date.now() - started));
4392
- } catch {
4393
- }
4394
4689
  }
4395
4690
  }
4396
4691
 
4692
+ // src/internal/capture/record-handoff.ts
4693
+ var fs6 = __toESM(require("fs"));
4694
+ var net = __toESM(require("net"));
4695
+ var path9 = __toESM(require("path"));
4696
+
4697
+ // ../core/src/ide-recorder.ts
4698
+ var IDE_RECORDER_BINDING = "__piwiRecorder";
4699
+ var IDE_DISPATCH_GLOBAL = "__piwiIdeDispatch";
4700
+ var RECORD_CHANNEL_ENV = "PIWI_RECORD_CHANNEL";
4701
+ var MAX_HANDOFF_LINE = 64e6;
4702
+ function parseRecordChannel(value) {
4703
+ const text = value?.trim() ?? "";
4704
+ const hash = text.lastIndexOf("#");
4705
+ if (hash <= 0) return null;
4706
+ const token = text.slice(hash + 1);
4707
+ const path15 = text.slice(0, hash);
4708
+ return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path15.includes("\0") ? { path: path15, token } : null;
4709
+ }
4710
+ function isObject2(value) {
4711
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4712
+ }
4713
+ function parseHandoffToTest(line) {
4714
+ let value;
4715
+ try {
4716
+ value = JSON.parse(line);
4717
+ } catch {
4718
+ return null;
4719
+ }
4720
+ if (!isObject2(value)) return null;
4721
+ switch (value.type) {
4722
+ case "refused":
4723
+ return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
4724
+ case "load":
4725
+ return typeof value.bundle === "string" && value.bundle ? { type: "load", bundle: value.bundle } : null;
4726
+ case "answer":
4727
+ return typeof value.call === "number" ? { type: "answer", call: value.call, value: value.value } : null;
4728
+ case "dispatch":
4729
+ return { type: "dispatch", message: value.message };
4730
+ case "stop":
4731
+ return { type: "stop" };
4732
+ default:
4733
+ return null;
4734
+ }
4735
+ }
4736
+ function lineReader(onLine, onOverflow) {
4737
+ let buffered = "";
4738
+ return (chunk) => {
4739
+ buffered += chunk;
4740
+ for (let end = buffered.indexOf("\n"); end >= 0; end = buffered.indexOf("\n")) {
4741
+ const line = buffered.slice(0, end);
4742
+ buffered = buffered.slice(end + 1);
4743
+ if (line.trim()) onLine(line);
4744
+ }
4745
+ if (buffered.length > MAX_HANDOFF_LINE) {
4746
+ buffered = "";
4747
+ onOverflow();
4748
+ }
4749
+ };
4750
+ }
4751
+
4752
+ // src/internal/capture/record-handoff.ts
4753
+ var START_WAIT_MS = 6e4;
4754
+ var NEW_TAB_NOTICE = "A new tab opened: what you do there is not recorded yet. Record in the first tab.";
4755
+ var relays = /* @__PURE__ */ new WeakMap();
4756
+ function idleAnswer(payload) {
4757
+ const request = payload ?? {};
4758
+ if (request.kind === "local-get") return {};
4759
+ if (request.kind !== "message") return null;
4760
+ if (request.message?.type === "piwi-ping") return { ok: true };
4761
+ if (request.message?.type === "piwi-session-storage") {
4762
+ return request.message.op === "get" ? { ok: true, items: {} } : { ok: true };
4763
+ }
4764
+ return { ok: false, error: "No recording runs in this page." };
4765
+ }
4766
+ function canRecordFromHere(channel = process.env[RECORD_CHANNEL_ENV]) {
4767
+ return parseRecordChannel(channel) !== null;
4768
+ }
4769
+ function handoffPlace(location, cwd = process.cwd()) {
4770
+ const m = /^(.+):(\d+):\d+$/.exec(location);
4771
+ if (!m) return null;
4772
+ return { file: path9.resolve(cwd, m[1]), line: Number(m[2]) };
4773
+ }
4774
+ async function recordFromHere(options) {
4775
+ const channel = parseRecordChannel(options.channel ?? process.env[RECORD_CHANNEL_ENV]);
4776
+ const place = handoffPlace(options.location);
4777
+ if (!channel || !place) return "this run names no editor to record into.";
4778
+ const { page, testInfo, ended } = options;
4779
+ if (ended?.aborted) return "the test ended.";
4780
+ const use = testInfo.project?.use ?? {};
4781
+ const context = page.context();
4782
+ return new Promise((resolve5) => {
4783
+ const socket = net.connect(channel.path);
4784
+ socket.setEncoding("utf8");
4785
+ const answers = /* @__PURE__ */ new Map();
4786
+ let calls = 0;
4787
+ let attached = false;
4788
+ let recording = false;
4789
+ let settled = false;
4790
+ let initScript;
4791
+ const send = (message) => {
4792
+ if (!socket.destroyed) socket.write(`${JSON.stringify(message)}
4793
+ `);
4794
+ };
4795
+ const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
4796
+ const onGone = () => {
4797
+ if (attached) send({ type: "closed" });
4798
+ finish("the page closed.");
4799
+ };
4800
+ const onEnded = () => {
4801
+ try {
4802
+ if (recording) {
4803
+ void internalCall(
4804
+ page,
4805
+ () => page.evaluate(
4806
+ ([name, value]) => {
4807
+ const dispatch = globalThis[name];
4808
+ if (typeof dispatch === "function") dispatch(value);
4809
+ },
4810
+ [IDE_DISPATCH_GLOBAL, { type: "piwi-recording-stopped" }]
4811
+ )
4812
+ ).catch(() => void 0);
4813
+ }
4814
+ if (attached) send({ type: "closed" });
4815
+ finish("the test ended.");
4816
+ } catch {
4817
+ }
4818
+ };
4819
+ page.once("close", onGone);
4820
+ page.once("crash", onGone);
4821
+ ended?.addEventListener("abort", onEnded, { once: true });
4822
+ const startWait = setTimeout(() => finish("the editor did not start the recording."), START_WAIT_MS);
4823
+ const release = () => {
4824
+ const relay = relays.get(context);
4825
+ if (relay && relay.recording === relayed) relay.recording = null;
4826
+ void Promise.resolve(initScript?.dispose?.()).catch(() => void 0);
4827
+ initScript = void 0;
4828
+ };
4829
+ const finish = (said) => {
4830
+ if (settled) return;
4831
+ settled = true;
4832
+ clearTimeout(startWait);
4833
+ release();
4834
+ for (const [call, answer] of answers) {
4835
+ answers.delete(call);
4836
+ answer(null);
4837
+ }
4838
+ context.off("page", onPage);
4839
+ page.off("close", onGone);
4840
+ page.off("crash", onGone);
4841
+ ended?.removeEventListener("abort", onEnded);
4842
+ socket.end();
4843
+ resolve5(recording ? null : said);
4844
+ };
4845
+ const relayed = (source, payload) => new Promise((answer) => {
4846
+ const call = ++calls;
4847
+ answers.set(call, answer);
4848
+ const outside = source.page !== page || source.frame !== page.mainFrame();
4849
+ send({ type: "binding", call, outside, payload });
4850
+ });
4851
+ const load = async (bundle) => {
4852
+ if (!/^https?:/i.test(page.url())) {
4853
+ const why = `the recorder records http(s) pages only, and this page is ${page.url() || "blank"}.`;
4854
+ send({ type: "notice", message: `Nothing can be recorded here: ${why}` });
4855
+ finish(why);
4856
+ return;
4857
+ }
4858
+ let relay = relays.get(context);
4859
+ if (!relay) {
4860
+ const created = { recording: null, lingering: false };
4861
+ await context.exposeBinding(
4862
+ IDE_RECORDER_BINDING,
4863
+ (source, payload) => created.recording ? created.recording(source, payload) : idleAnswer(payload)
4864
+ );
4865
+ relays.set(context, created);
4866
+ relay = created;
4867
+ }
4868
+ if (settled) return;
4869
+ relay.recording = relayed;
4870
+ if (!relay.lingering) {
4871
+ initScript = await context.addInitScript({ path: bundle });
4872
+ if (typeof initScript?.dispose !== "function") relay.lingering = true;
4873
+ }
4874
+ if (settled) return release();
4875
+ await page.evaluate(fs6.readFileSync(bundle, "utf-8"));
4876
+ if (settled) return release();
4877
+ recording = true;
4878
+ clearTimeout(startWait);
4879
+ context.on("page", onPage);
4880
+ send({ type: "loaded" });
4881
+ };
4882
+ socket.on("connect", () => {
4883
+ attached = true;
4884
+ send({
4885
+ type: "attach",
4886
+ token: channel.token,
4887
+ file: place.file,
4888
+ line: place.line,
4889
+ reason: options.reason,
4890
+ testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
4891
+ baseURL: typeof use.baseURL === "string" ? use.baseURL : null
4892
+ });
4893
+ });
4894
+ socket.on(
4895
+ "data",
4896
+ lineReader(
4897
+ (line) => {
4898
+ const message = parseHandoffToTest(line);
4899
+ if (!message || settled) return;
4900
+ switch (message.type) {
4901
+ case "refused":
4902
+ return finish(message.message);
4903
+ case "load":
4904
+ return void load(message.bundle).catch(
4905
+ (error) => finish(`the recorder could not be loaded: ${error instanceof Error ? error.message : String(error)}`)
4906
+ );
4907
+ case "answer": {
4908
+ const answer = answers.get(message.call);
4909
+ answers.delete(message.call);
4910
+ return answer?.(message.value);
4911
+ }
4912
+ case "dispatch":
4913
+ return void page.evaluate(
4914
+ ([name, value]) => {
4915
+ const dispatch = globalThis[name];
4916
+ if (typeof dispatch === "function") dispatch(value);
4917
+ },
4918
+ [IDE_DISPATCH_GLOBAL, message.message]
4919
+ ).catch(() => void 0);
4920
+ case "stop":
4921
+ return finish(null);
4922
+ }
4923
+ },
4924
+ () => finish("the editor sent a message too long to read.")
4925
+ )
4926
+ );
4927
+ socket.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
4928
+ socket.on("close", () => finish("the editor closed the recording."));
4929
+ });
4930
+ }
4931
+
4397
4932
  // src/internal/collect/coverage-settings.ts
4398
4933
  var SERVER_VALUES = /* @__PURE__ */ new Set(["files", "functions", "lines"]);
4399
4934
  function isServerGranularity(value) {
@@ -4409,7 +4944,7 @@ function browserFunctionsInWorker() {
4409
4944
  function browserFunctionCoverage(files) {
4410
4945
  return sanitizeCoverage({
4411
4946
  v: 1,
4412
- files: [...files].map(([path13, functions]) => ({ path: path13, runtime: "browser", functions }))
4947
+ files: [...files].map(([path15, functions]) => ({ path: path15, runtime: "browser", functions }))
4413
4948
  });
4414
4949
  }
4415
4950
  function newKnownFunctions(known, sent) {
@@ -4422,19 +4957,19 @@ function newKnownFunctions(known, sent) {
4422
4957
  budget = MAX_COVERAGE_RANGES;
4423
4958
  };
4424
4959
  for (const [raw, ranges] of known) {
4425
- const path13 = normalizeCoveragePath(raw);
4426
- if (path13 === null) continue;
4427
- let named = sent.get(path13);
4960
+ const path15 = normalizeCoveragePath(raw);
4961
+ if (path15 === null) continue;
4962
+ let named = sent.get(path15);
4428
4963
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
4429
4964
  if (fresh.length === 0) continue;
4430
- if (!named) sent.set(path13, named = /* @__PURE__ */ new Set());
4965
+ if (!named) sent.set(path15, named = /* @__PURE__ */ new Set());
4431
4966
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
4432
4967
  while (fresh.length > 0) {
4433
4968
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
4434
4969
  const part = fresh.slice(0, budget);
4435
4970
  fresh = fresh.slice(part.length);
4436
4971
  budget -= part.length;
4437
- files.push({ path: path13, allFunctions: part });
4972
+ files.push({ path: path15, allFunctions: part });
4438
4973
  }
4439
4974
  }
4440
4975
  close();
@@ -4445,9 +4980,9 @@ function serverCoverageInWorker() {
4445
4980
  }
4446
4981
 
4447
4982
  // src/internal/support/aria-sampling.ts
4448
- var path6 = __toESM(require("path"));
4983
+ var path10 = __toESM(require("path"));
4449
4984
  var os3 = __toESM(require("os"));
4450
- var fs4 = __toESM(require("fs"));
4985
+ var fs7 = __toESM(require("fs"));
4451
4986
 
4452
4987
  // src/internal/support/instance-id.ts
4453
4988
  var crypto = __toESM(require("crypto"));
@@ -4461,14 +4996,14 @@ function ariaSampleIdentity(filePath, title) {
4461
4996
  return `${filePath}\0${title}`;
4462
4997
  }
4463
4998
  function getAriaSampleFilePath(projectName) {
4464
- return path6.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
4999
+ return path10.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
4465
5000
  }
4466
5001
  var cachedSets = /* @__PURE__ */ new Map();
4467
5002
  function loadAriaSampleSet(projectName) {
4468
5003
  if (cachedSets.has(projectName)) return cachedSets.get(projectName);
4469
5004
  let set = null;
4470
5005
  try {
4471
- const raw = fs4.readFileSync(getAriaSampleFilePath(projectName), "utf8");
5006
+ const raw = fs7.readFileSync(getAriaSampleFilePath(projectName), "utf8");
4472
5007
  const parsed2 = JSON.parse(raw);
4473
5008
  if (parsed2.projectName === projectName && Array.isArray(parsed2.identities)) {
4474
5009
  set = new Set(parsed2.identities.filter((x) => typeof x === "string"));
@@ -4489,97 +5024,10 @@ function isDueForAriaSample(testInfo) {
4489
5024
  );
4490
5025
  }
4491
5026
 
4492
- // src/internal/capture/quiet-capture.ts
4493
- var import_node_module = require("module");
4494
- var path8 = __toESM(require("path"));
4495
-
4496
- // src/internal/support/reporter-version.ts
4497
- var fs5 = __toESM(require("fs"));
4498
- var path7 = __toESM(require("path"));
4499
- var CANDIDATE_OFFSETS = ["..", "../../.."];
4500
- function findOwnPackageJson(fromDir) {
4501
- for (const offset of CANDIDATE_OFFSETS) {
4502
- try {
4503
- const pkgPath = path7.resolve(fromDir, offset, "package.json");
4504
- const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
4505
- const { name, version } = pkg;
4506
- if (name === "@piwitests/reporter" && typeof version === "string")
4507
- return { version, root: path7.dirname(pkgPath) };
4508
- } catch {
4509
- }
4510
- }
4511
- return void 0;
4512
- }
4513
-
4514
- // src/internal/capture/quiet-capture.ts
4515
- var BOXED_PREFIX_EXPORTS = [
4516
- [
4517
- "playwright-core/lib/coreBundle",
4518
- (mod) => mod?.utils?.setBoxedStackPrefixes
4519
- ],
4520
- ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
4521
- ];
4522
- var boxing;
4523
- var resolvedPlaywrightPackageJson;
4524
- function resolvePlaywrightPackageJson() {
4525
- if (resolvedPlaywrightPackageJson !== void 0) return resolvedPlaywrightPackageJson;
4526
- try {
4527
- const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
4528
- resolvedPlaywrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
4529
- } catch {
4530
- resolvedPlaywrightPackageJson = null;
4531
- }
4532
- return resolvedPlaywrightPackageJson;
4533
- }
4534
- function resolveBoxing() {
4535
- try {
4536
- const root = findOwnPackageJson(__dirname)?.root;
4537
- if (!root) return null;
4538
- const playwrightPackageJson = resolvePlaywrightPackageJson();
4539
- if (!playwrightPackageJson) return null;
4540
- const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
4541
- for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
4542
- let set;
4543
- try {
4544
- set = pick(requireFromPlaywright(moduleId));
4545
- } catch {
4546
- continue;
4547
- }
4548
- if (typeof set !== "function") continue;
4549
- return {
4550
- set,
4551
- prefixes: [path8.dirname(playwrightPackageJson), path8.join(root, "dist") + path8.sep]
4552
- };
4553
- }
4554
- } catch {
4555
- }
4556
- return null;
4557
- }
4558
- function boxCaptureFrames(extraPrefixes = []) {
4559
- const first = boxing === void 0;
4560
- if (first) boxing = resolveBoxing();
4561
- if (!boxing) return;
4562
- const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
4563
- if (!first && added.length === 0) return;
4564
- boxing.prefixes.push(...added);
4565
- try {
4566
- boxing.set([...boxing.prefixes]);
4567
- } catch {
4568
- }
4569
- }
4570
- function callMethod(target, prop, args) {
4571
- return target[prop](...args);
4572
- }
4573
- function internalCall(owner, run) {
4574
- const wrap = owner?._wrapApiCall;
4575
- if (typeof wrap !== "function") return run();
4576
- return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
4577
- }
4578
-
4579
5027
  // src/internal/capture/code-reach.ts
4580
5028
  var crypto2 = __toESM(require("crypto"));
4581
- var fs6 = __toESM(require("fs"));
4582
- var path9 = __toESM(require("path"));
5029
+ var fs8 = __toESM(require("fs"));
5030
+ var path11 = __toESM(require("path"));
4583
5031
  var import_node_child_process = require("child_process");
4584
5032
  var MAX_MAP_BYTES = 20 * 1024 * 1024;
4585
5033
  var MAP_TIMEOUT_MS = 5e3;
@@ -4622,18 +5070,18 @@ function repositoryRoot(from) {
4622
5070
  }).trim();
4623
5071
  } catch {
4624
5072
  }
4625
- cachedRepoRoot = { from, root: path9.resolve(root) };
5073
+ cachedRepoRoot = { from, root: path11.resolve(root) };
4626
5074
  return cachedRepoRoot.root;
4627
5075
  }
4628
5076
  function codeReachRoots(configDir, configured) {
4629
5077
  const repoRoot = repositoryRoot(configDir);
4630
- const resolve3 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path9.resolve(configDir, r));
4631
- const global = Array.isArray(configured) ? resolve3(configured) : resolve3(configured?.["*"]);
5078
+ const resolve5 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path11.resolve(configDir, r));
5079
+ const global = Array.isArray(configured) ? resolve5(configured) : resolve5(configured?.["*"]);
4632
5080
  const byOrigin = /* @__PURE__ */ new Map();
4633
5081
  if (configured && !Array.isArray(configured)) {
4634
5082
  for (const [key, dirs] of Object.entries(configured)) {
4635
5083
  const origin = key === "*" ? null : originOf2(key);
4636
- const resolved = resolve3(dirs);
5084
+ const resolved = resolve5(dirs);
4637
5085
  if (origin && resolved.length) byOrigin.set(origin, resolved);
4638
5086
  }
4639
5087
  }
@@ -4656,7 +5104,7 @@ function scriptRoots(roots, scriptUrl, pageUrl) {
4656
5104
  }
4657
5105
  function isFile(file) {
4658
5106
  try {
4659
- return fs6.statSync(file).isFile();
5107
+ return fs8.statSync(file).isFile();
4660
5108
  } catch {
4661
5109
  return false;
4662
5110
  }
@@ -4664,10 +5112,10 @@ function isFile(file) {
4664
5112
  function resolveSourcePath(source, roots) {
4665
5113
  const normalized = normalizeSourcePath(source);
4666
5114
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
4667
- if (path9.isAbsolute(normalized) && isFile(normalized)) return path9.resolve(normalized);
5115
+ if (path11.isAbsolute(normalized) && isFile(normalized)) return path11.resolve(normalized);
4668
5116
  const relative7 = normalized.replace(/^\/+/, "");
4669
5117
  for (const root of roots) {
4670
- const candidate = path9.resolve(root, relative7);
5118
+ const candidate = path11.resolve(root, relative7);
4671
5119
  if (isFile(candidate)) return candidate;
4672
5120
  }
4673
5121
  return null;
@@ -4699,7 +5147,7 @@ function fileForUrl(url, roots, bases) {
4699
5147
  return file;
4700
5148
  }
4701
5149
  function relativeToRepo(file, repoRoot) {
4702
- return path9.relative(repoRoot, file).split(path9.sep).join("/");
5150
+ return path11.relative(repoRoot, file).split(path11.sep).join("/");
4703
5151
  }
4704
5152
  function scriptVersion(entry) {
4705
5153
  return entry.hash ?? crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -4762,10 +5210,10 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
4762
5210
  const files = /* @__PURE__ */ new Map();
4763
5211
  const known = /* @__PURE__ */ new Map();
4764
5212
  const addTo = (into, file, functions) => {
4765
- const path13 = relativeToRepo(file, roots.repoRoot);
4766
- const listed = into.get(path13);
5213
+ const path15 = relativeToRepo(file, roots.repoRoot);
5214
+ const listed = into.get(path15);
4767
5215
  if (listed) listed.push(...functions);
4768
- else into.set(path13, [...functions]);
5216
+ else into.set(path15, [...functions]);
4769
5217
  };
4770
5218
  const add = (file, functions) => addTo(files, file, functions);
4771
5219
  const bases = viteBases(entries);
@@ -4917,8 +5365,8 @@ function onScriptParsed(page, capture, event) {
4917
5365
  var READ_WAIT_MS = 5e3;
4918
5366
  function readCoverage(page, cdp) {
4919
5367
  let timer;
4920
- const late = new Promise((resolve3) => {
4921
- timer = setTimeout(() => resolve3([]), READ_WAIT_MS);
5368
+ const late = new Promise((resolve5) => {
5369
+ timer = setTimeout(() => resolve5([]), READ_WAIT_MS);
4922
5370
  });
4923
5371
  const taken = internalCall(page, () => cdp.send("Profiler.takePreciseCoverage")).then(
4924
5372
  (answer) => answer.result,
@@ -5028,20 +5476,20 @@ async function stopCodeReach(page) {
5028
5476
  }
5029
5477
 
5030
5478
  // src/internal/capture/resource-ledger.ts
5031
- var fs8 = __toESM(require("fs"));
5032
- var path11 = __toESM(require("path"));
5479
+ var fs10 = __toESM(require("fs"));
5480
+ var path13 = __toESM(require("path"));
5033
5481
  var import_node_module2 = require("module");
5034
5482
 
5035
5483
  // src/internal/capture/worker-metrics.ts
5036
5484
  var import_node_perf_hooks = require("perf_hooks");
5037
5485
 
5038
5486
  // src/internal/support/system-readers.ts
5039
- var fs7 = __toESM(require("fs"));
5040
- var path10 = __toESM(require("path"));
5487
+ var fs9 = __toESM(require("fs"));
5488
+ var path12 = __toESM(require("path"));
5041
5489
  var CLK_TCK = 100;
5042
5490
  function readText(file) {
5043
5491
  try {
5044
- return fs7.readFileSync(file, "utf8");
5492
+ return fs9.readFileSync(file, "utf8");
5045
5493
  } catch {
5046
5494
  return null;
5047
5495
  }
@@ -5123,45 +5571,45 @@ var ProcFs = class {
5123
5571
  }
5124
5572
  /** Whether this looks like a Linux `/proc` at all. */
5125
5573
  available() {
5126
- return fs7.existsSync(path10.join(this.root, "stat"));
5574
+ return fs9.existsSync(path12.join(this.root, "stat"));
5127
5575
  }
5128
5576
  /** Every process, keyed by pid. */
5129
5577
  table() {
5130
5578
  const out = /* @__PURE__ */ new Map();
5131
5579
  let names;
5132
5580
  try {
5133
- names = fs7.readdirSync(this.root);
5581
+ names = fs9.readdirSync(this.root);
5134
5582
  } catch {
5135
5583
  return out;
5136
5584
  }
5137
5585
  for (const name of names) {
5138
5586
  if (!/^\d+$/.test(name)) continue;
5139
- const text = readText(path10.join(this.root, name, "stat"));
5587
+ const text = readText(path12.join(this.root, name, "stat"));
5140
5588
  const stat = text ? parseProcStat(Number(name), text) : null;
5141
5589
  if (stat) out.set(stat.pid, stat);
5142
5590
  }
5143
5591
  return out;
5144
5592
  }
5145
5593
  stat(pid) {
5146
- const text = readText(path10.join(this.root, String(pid), "stat"));
5594
+ const text = readText(path12.join(this.root, String(pid), "stat"));
5147
5595
  return text ? parseProcStat(pid, text) : null;
5148
5596
  }
5149
5597
  runWaitMs(pid) {
5150
- const text = readText(path10.join(this.root, String(pid), "schedstat"));
5598
+ const text = readText(path12.join(this.root, String(pid), "schedstat"));
5151
5599
  return text ? parseSchedstat(text) : null;
5152
5600
  }
5153
5601
  cmdline(pid) {
5154
- return (readText(path10.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
5602
+ return (readText(path12.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
5155
5603
  }
5156
5604
  /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
5157
5605
  statusKb(pid, key) {
5158
- const text = readText(path10.join(this.root, String(pid), "status"));
5606
+ const text = readText(path12.join(this.root, String(pid), "status"));
5159
5607
  return text ? parseKbField(text, key) : null;
5160
5608
  }
5161
5609
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
5162
5610
  async pssKb(pid) {
5163
5611
  try {
5164
- return parseKbField(await fs7.promises.readFile(path10.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
5612
+ return parseKbField(await fs9.promises.readFile(path12.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
5165
5613
  } catch {
5166
5614
  return null;
5167
5615
  }
@@ -5169,29 +5617,29 @@ var ProcFs = class {
5169
5617
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
5170
5618
  resetPeakRss(pid) {
5171
5619
  try {
5172
- fs7.writeFileSync(path10.join(this.root, String(pid), "clear_refs"), "5");
5620
+ fs9.writeFileSync(path12.join(this.root, String(pid), "clear_refs"), "5");
5173
5621
  return true;
5174
5622
  } catch {
5175
5623
  return false;
5176
5624
  }
5177
5625
  }
5178
5626
  cpuTicks() {
5179
- const text = readText(path10.join(this.root, "stat"));
5627
+ const text = readText(path12.join(this.root, "stat"));
5180
5628
  return text ? parseCpuTicks(text) : null;
5181
5629
  }
5182
5630
  /** Total stall time of a resource's pressure file, in µs. */
5183
5631
  pressureUs(resource) {
5184
- const text = readText(path10.join(this.root, "pressure", resource));
5632
+ const text = readText(path12.join(this.root, "pressure", resource));
5185
5633
  return text ? parsePsiTotal(text) : null;
5186
5634
  }
5187
5635
  memAvailableKb() {
5188
- const text = readText(path10.join(this.root, "meminfo"));
5636
+ const text = readText(path12.join(this.root, "meminfo"));
5189
5637
  return text ? parseKbField(text, "MemAvailable") : null;
5190
5638
  }
5191
5639
  /** Open file descriptors of a process. */
5192
5640
  fdCount(pid) {
5193
5641
  try {
5194
- return fs7.readdirSync(path10.join(this.root, String(pid), "fd")).length;
5642
+ return fs9.readdirSync(path12.join(this.root, String(pid), "fd")).length;
5195
5643
  } catch {
5196
5644
  return null;
5197
5645
  }
@@ -5353,10 +5801,10 @@ var PAGE_READ_TIMEOUT_MS = 1e3;
5353
5801
  var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
5354
5802
  var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
5355
5803
  function relativePath(file) {
5356
- return path11.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path11.sep).join("/");
5804
+ return path13.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path13.sep).join("/");
5357
5805
  }
5358
5806
  function isLibraryFile(file) {
5359
- return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path11.sep}node_modules${path11.sep}`) || file.includes("/node_modules/") || file === __filename;
5807
+ return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path13.sep}node_modules${path13.sep}`) || file.includes("/node_modules/") || file === __filename;
5360
5808
  }
5361
5809
  function userSite(stack) {
5362
5810
  for (const line of (stack ?? "").split("\n").slice(1)) {
@@ -5404,7 +5852,7 @@ function playwrightTestInfoSource() {
5404
5852
  try {
5405
5853
  const packageJson = resolvePlaywrightPackageJson();
5406
5854
  if (!packageJson) return () => null;
5407
- const globals = (0, import_node_module2.createRequire)(packageJson)(path11.join(path11.dirname(packageJson), "lib", "globals.js"));
5855
+ const globals = (0, import_node_module2.createRequire)(packageJson)(path13.join(path13.dirname(packageJson), "lib", "globals.js"));
5408
5856
  const read = globals.currentTestInfo;
5409
5857
  if (typeof read !== "function") return () => null;
5410
5858
  return () => {
@@ -5433,15 +5881,15 @@ function countHandles() {
5433
5881
  async function settledHandles(start) {
5434
5882
  const end = countHandles();
5435
5883
  if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
5436
- await new Promise((resolve3) => setTimeout(resolve3, 0));
5884
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
5437
5885
  return countHandles();
5438
5886
  }
5439
5887
  function settleWithin(promise, ms) {
5440
5888
  let timer;
5441
5889
  return Promise.race([
5442
5890
  promise.catch(() => void 0),
5443
- new Promise((resolve3) => {
5444
- timer = setTimeout(resolve3, ms);
5891
+ new Promise((resolve5) => {
5892
+ timer = setTimeout(resolve5, ms);
5445
5893
  })
5446
5894
  ]).finally(() => clearTimeout(timer));
5447
5895
  }
@@ -5570,7 +6018,7 @@ var ResourceLedger = class {
5570
6018
  const census = this.census(null);
5571
6019
  if (this.resultsFile) {
5572
6020
  try {
5573
- fs8.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
6021
+ fs10.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
5574
6022
  `);
5575
6023
  } catch {
5576
6024
  }
@@ -6187,6 +6635,13 @@ function createSink() {
6187
6635
  pickOffered: false,
6188
6636
  userPick: null,
6189
6637
  pause: null,
6638
+ locatorCalls: /* @__PURE__ */ new Map(),
6639
+ stops: [],
6640
+ shownStops: /* @__PURE__ */ new Map(),
6641
+ released: [],
6642
+ releasedRequests: /* @__PURE__ */ new Set(),
6643
+ onReleasedRequestsEnded: null,
6644
+ releasedQuietSince: 0,
6190
6645
  locatorPages: new LocatorPageLog(),
6191
6646
  codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
6192
6647
  serverCoverage: /* @__PURE__ */ new Map(),
@@ -6212,7 +6667,7 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
6212
6667
  async function startCodeReachOnOpenPages() {
6213
6668
  if (!browserCoverageInWorker() || OPEN_PAGES.size === 0) return;
6214
6669
  const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
6215
- await Promise.race([started, new Promise((resolve3) => setTimeout(resolve3, CODE_REACH_START_WAIT_MS).unref())]);
6670
+ await Promise.race([started, new Promise((resolve5) => setTimeout(resolve5, CODE_REACH_START_WAIT_MS).unref())]);
6216
6671
  }
6217
6672
  function configuredCodeReachRoots() {
6218
6673
  const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
@@ -6226,7 +6681,7 @@ function configuredCodeReachRoots() {
6226
6681
  }
6227
6682
  } catch {
6228
6683
  }
6229
- return raw.split(path12.delimiter).filter(Boolean);
6684
+ return raw.split(path14.delimiter).filter(Boolean);
6230
6685
  }
6231
6686
  async function stopSinkCodeReach(sink, closing) {
6232
6687
  const stopping = [];
@@ -6275,8 +6730,8 @@ async function drainPendingProbes(capMs) {
6275
6730
  let cap;
6276
6731
  await Promise.race([
6277
6732
  Promise.allSettled(PENDING_PROBES),
6278
- new Promise((resolve3) => {
6279
- cap = setTimeout(resolve3, capMs);
6733
+ new Promise((resolve5) => {
6734
+ cap = setTimeout(resolve5, capMs);
6280
6735
  })
6281
6736
  ]);
6282
6737
  clearTimeout(cap);
@@ -6316,10 +6771,10 @@ async function readWebVitals(page) {
6316
6771
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
6317
6772
  paint[key] = Math.round(entry.startTime);
6318
6773
  }
6319
- const readBuffered = (type, extra) => new Promise((resolve3) => {
6774
+ const readBuffered = (type, extra) => new Promise((resolve5) => {
6320
6775
  try {
6321
6776
  const PO = globalThis.PerformanceObserver;
6322
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve3(null);
6777
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve5(null);
6323
6778
  const out = [];
6324
6779
  const po = new PO((list) => out.push(...list.getEntries()));
6325
6780
  po.observe({ type, buffered: true, ...extra });
@@ -6329,10 +6784,10 @@ async function readWebVitals(page) {
6329
6784
  po.disconnect();
6330
6785
  } catch {
6331
6786
  }
6332
- resolve3(out);
6787
+ resolve5(out);
6333
6788
  }, 0);
6334
6789
  } catch {
6335
- resolve3(null);
6790
+ resolve5(null);
6336
6791
  }
6337
6792
  });
6338
6793
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -6447,12 +6902,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
6447
6902
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
6448
6903
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
6449
6904
  function withTimeout(promise, ms) {
6450
- return new Promise((resolve3, reject) => {
6905
+ return new Promise((resolve5, reject) => {
6451
6906
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
6452
6907
  promise.then(
6453
6908
  (value) => {
6454
6909
  clearTimeout(timer);
6455
- resolve3(value);
6910
+ resolve5(value);
6456
6911
  },
6457
6912
  (error) => {
6458
6913
  clearTimeout(timer);
@@ -6567,6 +7022,7 @@ async function maybeOpenPicker(sink, closing) {
6567
7022
  var INSTRUMENTED_PAGES = /* @__PURE__ */ new WeakSet();
6568
7023
  var OPEN_PAGES = /* @__PURE__ */ new Set();
6569
7024
  var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
7025
+ var OPEN_CONTEXTS = /* @__PURE__ */ new Set();
6570
7026
  var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
6571
7027
  var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
6572
7028
  var ACTION_METHOD_SET = new Set(ACTION_METHODS);
@@ -6687,8 +7143,20 @@ function recordLocatorPage(sink, page, locator, location, arrival) {
6687
7143
  } catch {
6688
7144
  }
6689
7145
  }
6690
- function pausesAt(pause, callerLocation) {
6691
- return !pause.finished && (pause.next || pause.set.matches(callerLocation));
7146
+ function knowLocatorCall(sink, page, target, location, origin) {
7147
+ if (!sink?.pause || !location) return null;
7148
+ let settle = () => {
7149
+ };
7150
+ const settled = new Promise((resolve5) => settle = resolve5);
7151
+ const call = { page, target, location, origin, picked: false, settled, settle };
7152
+ sink.locatorCalls.set(callSiteLine(location), call);
7153
+ return call;
7154
+ }
7155
+ function forgetLocatorCall(sink, call) {
7156
+ if (!sink || !call) return;
7157
+ call.settle();
7158
+ const key = callSiteLine(call.location);
7159
+ if (sink.locatorCalls.get(key) === call) sink.locatorCalls.delete(key);
6692
7160
  }
6693
7161
  function locatorSource(target, method, args) {
6694
7162
  try {
@@ -6698,41 +7166,219 @@ function locatorSource(target, method, args) {
6698
7166
  }
6699
7167
  return renderFailing({ method, args });
6700
7168
  }
6701
- async function pauseBefore(sink, page, target, callerLocation, action, origin) {
6702
- const pause = sink.pause;
7169
+ var PAGE_PAUSES = /* @__PURE__ */ new WeakMap();
7170
+ var PLAYWRIGHT_PAUSES = /* @__PURE__ */ new WeakMap();
7171
+ async function armBreakpoints(contexts) {
7172
+ const state = currentSink?.pause;
7173
+ if (!state || state.finished || state.set.size === 0) return;
7174
+ await Promise.all(
7175
+ [...contexts].map(
7176
+ (context) => context && watchStops(context, onDebuggerStop) ? steerDebugger(context, "pause") : void 0
7177
+ )
7178
+ );
7179
+ }
7180
+ function onDebuggerStop(context) {
7181
+ const details = contextDebugger(context)?.pausedDetails();
7182
+ if (!details || PLAYWRIGHT_PAUSES.has(context)) return;
7183
+ const sink = currentSink;
7184
+ const state = sink?.pause ?? null;
7185
+ void (async () => {
7186
+ let how = "resume";
7187
+ try {
7188
+ if (!sink || !state) return;
7189
+ const callSite = stopSite(sink, details.location);
7190
+ const pagePause = PAGE_PAUSES.get(context);
7191
+ const decided = pagePause ? { show: "page-pause" } : atStop(state, callSite);
7192
+ if ("go" in decided) {
7193
+ how = decided.go;
7194
+ return;
7195
+ }
7196
+ how = goOn(state, await showStop(sink, context, details.title, callSite ?? pagePause, decided.show));
7197
+ } catch {
7198
+ if (state) how = goOn(state);
7199
+ } finally {
7200
+ await steerDebugger(context, how);
7201
+ }
7202
+ })();
7203
+ }
7204
+ function stopSite(sink, location) {
7205
+ if (path14.dirname(path14.resolve(location.file)) !== __dirname) return stopCallSite(location);
7206
+ return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
7207
+ }
7208
+ function stopPage(sink, context, call) {
7209
+ for (const page of [call?.page, sink.lastActivePage]) {
7210
+ if (page && !isPageClosed(page) && pageContext(page) === context) return page;
7211
+ }
7212
+ const open = context.pages().filter((page) => !isPageClosed(page));
7213
+ return open[open.length - 1] ?? null;
7214
+ }
7215
+ async function showStop(sink, context, title, location, reason) {
6703
7216
  const testInfo = sink.testInfo;
6704
- if (!pause || !testInfo || !callerLocation) return;
6705
- pause.next = false;
6706
- try {
6707
- const locator = locatorSource(target, origin.method, origin.args);
6708
- const onPick = async (picked) => {
6709
- const pick = {
6710
- failing: { method: origin.method, args: origin.args, rendered: locator, location: callerLocation },
7217
+ const call = sink.locatorCalls.get(callSiteLine(location));
7218
+ const page = stopPage(sink, context, call);
7219
+ if (!testInfo || !page) return void 0;
7220
+ const onPick = async (picked) => {
7221
+ if (call) {
7222
+ call.picked = true;
7223
+ const { method, args } = call.origin;
7224
+ const rendered = locatorSource(call.target, method, args);
7225
+ applyPickToSnapshots(sink.capturedLocators, {
7226
+ failing: { method, args, rendered, location: call.location },
6711
7227
  ...picked
6712
- };
6713
- applyPickToSnapshots(sink.capturedLocators, pick);
6714
- sink.probedLocations.add(callerLocation);
6715
- const place = callerLocation.slice(0, callerLocation.lastIndexOf(":"));
6716
- console.log(`[piwi] Locator picked at ${place}: ${picked.picked.locator}`);
6717
- await postPickToEditor(
6718
- process.env[PIWI_LOCAL_DEBUG_ENV.editorSend],
6719
- pickPayload(picked.picked.locator, callerLocation)
6720
- );
6721
- };
6722
- const choice = await internalCall(
7228
+ });
7229
+ sink.probedLocations.add(call.location);
7230
+ }
7231
+ console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
7232
+ await postPickToEditor(process.env[PIWI_LOCAL_DEBUG_ENV.editorSend], pickPayload(picked.picked.locator, location));
7233
+ };
7234
+ const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
7235
+ sink.stops.push(record);
7236
+ const end = new AbortController();
7237
+ const shown = withTestTimeoutHeld(
7238
+ testInfo,
7239
+ () => internalCall(
6723
7240
  page,
6724
7241
  () => pauseHere(
6725
7242
  page,
6726
- testInfo,
6727
- { location: callerLocation, action, locator, target },
7243
+ {
7244
+ location,
7245
+ call: title,
7246
+ attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
7247
+ target: call?.page === page ? call.target : null
7248
+ },
6728
7249
  { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
6729
- onPick
7250
+ onPick,
7251
+ recorderAt(page, testInfo, location, reason === "page-pause" ? "pause" : "breakpoint"),
7252
+ end.signal
6730
7253
  )
6731
- );
6732
- if (choice === "step") pause.next = true;
6733
- else if (choice === "finish") pause.finished = true;
6734
- } catch {
7254
+ )
7255
+ ).finally(() => {
7256
+ if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
7257
+ else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
7258
+ sink.shownStops.delete(end);
7259
+ });
7260
+ const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
7261
+ sink.shownStops.set(end, { page, record, done: shown.catch(() => void 0), heldAt });
7262
+ const answer = await shown;
7263
+ if (!end.signal.aborted) return answer;
7264
+ return end.signal.reason === "closed" ? "resume" : "finish";
7265
+ }
7266
+ var END_STOP_MS = 2e3;
7267
+ var RELEASED_CALL_MS = 2e3;
7268
+ var RELEASED_QUIET_MS = 200;
7269
+ async function endShownStops(sink, why, closing) {
7270
+ const ending = [];
7271
+ let heldAt = Infinity;
7272
+ for (const [end, { page, record, done, heldAt: stopHeldAt }] of sink.shownStops) {
7273
+ if (closing && closing.page !== page && (!closing.context || pageContext(page) !== closing.context)) continue;
7274
+ if (!end.signal.aborted) {
7275
+ record.duration = Date.now() - record.startedAt;
7276
+ end.abort(why);
7277
+ if (why === "ended" && stopHeldAt !== null) heldAt = Math.min(heldAt, stopHeldAt);
7278
+ }
7279
+ ending.push(done);
7280
+ }
7281
+ if (sink.testInfo && heldAt !== Infinity) restoreTestTime(sink.testInfo, heldAt);
7282
+ await settledWithin(ending, END_STOP_MS);
7283
+ }
7284
+ function releaseCall(sink, settled) {
7285
+ sink.releasedQuietSince = Date.now();
7286
+ sink.released.push(settled.then(() => void (sink.releasedQuietSince = Date.now())));
7287
+ }
7288
+ function releasedRequestsEnded(sink) {
7289
+ if (sink.releasedRequests.size === 0) return Promise.resolve();
7290
+ return new Promise((resolve5) => sink.onReleasedRequestsEnded = resolve5);
7291
+ }
7292
+ async function awaitReleasedCalls(sink) {
7293
+ if (sink.released.length === 0) return;
7294
+ const deadline = Date.now() + RELEASED_CALL_MS;
7295
+ await settledWithin(sink.released, RELEASED_CALL_MS);
7296
+ for (let now = Date.now(); now < deadline; now = Date.now()) {
7297
+ if (sink.releasedRequests.size > 0) {
7298
+ await settledWithin([releasedRequestsEnded(sink)], deadline - now);
7299
+ continue;
7300
+ }
7301
+ const quiet = now - sink.releasedQuietSince;
7302
+ if (quiet >= RELEASED_QUIET_MS) return;
7303
+ await new Promise((resolve5) => setTimeout(resolve5, Math.min(RELEASED_QUIET_MS - quiet, deadline - now)));
7304
+ }
7305
+ }
7306
+ async function settledWithin(pending, ms) {
7307
+ if (pending.length === 0) return;
7308
+ let timer;
7309
+ await Promise.race([Promise.allSettled(pending), new Promise((resolve5) => timer = setTimeout(resolve5, ms))]);
7310
+ clearTimeout(timer);
7311
+ }
7312
+ function endStopsWithTestFunction(testInfo, sink) {
7313
+ const callbacks = testInfo._onDidFinishTestFunctionCallbacks;
7314
+ if (!(callbacks instanceof Set)) return;
7315
+ const theirs = [...callbacks];
7316
+ callbacks.clear();
7317
+ callbacks.add(() => endShownStops(sink, "ended"));
7318
+ for (const callback of theirs) callbacks.add(callback);
7319
+ }
7320
+ function recorderAt(page, testInfo, location, reason) {
7321
+ if (!canRecordFromHere()) return void 0;
7322
+ return async (ended) => {
7323
+ console.log(`[piwi] Recording from ${callSiteLine(location)}: the editor writes the steps.`);
7324
+ const why = await recordFromHere({ page, testInfo, location, reason, ended });
7325
+ if (ended?.aborted) console.log("[piwi] The recording ended with the test.");
7326
+ else
7327
+ console.log(
7328
+ why ? `[piwi] Nothing was recorded: ${why}` : "[piwi] The recording ended: the test is paused again."
7329
+ );
7330
+ };
7331
+ }
7332
+ async function pauseInPlace(page, playwrightPause, location) {
7333
+ const sink = currentSink;
7334
+ const testInfo = sink?.testInfo;
7335
+ const context = pageContext(page);
7336
+ if (!sink || !testInfo || !location || !context || !contextDebugger(context)) {
7337
+ return leaveToPlaywright(context, playwrightPause);
7338
+ }
7339
+ if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
7340
+ return leaveToPlaywright(context, playwrightPause);
7341
+ }
7342
+ sink.pause ??= { set: NO_BREAKPOINTS, step: false, finished: false };
7343
+ watchStops(context, onDebuggerStop);
7344
+ await steerDebugger(context, "pause");
7345
+ PAGE_PAUSES.set(context, location);
7346
+ try {
7347
+ await playwrightPause({ __testHookKeepTestTimeout: true });
7348
+ } finally {
7349
+ PAGE_PAUSES.delete(context);
7350
+ }
7351
+ }
7352
+ async function leaveToPlaywright(context, playwrightPause) {
7353
+ if (context) PLAYWRIGHT_PAUSES.set(context, (PLAYWRIGHT_PAUSES.get(context) ?? 0) + 1);
7354
+ try {
7355
+ await withInspector(() => playwrightPause());
7356
+ } finally {
7357
+ if (context) {
7358
+ const left = (PLAYWRIGHT_PAUSES.get(context) ?? 1) - 1;
7359
+ if (left > 0) PLAYWRIGHT_PAUSES.set(context, left);
7360
+ else PLAYWRIGHT_PAUSES.delete(context);
7361
+ }
7362
+ }
7363
+ }
7364
+ var PLAYWRIGHT_PAUSE = /* @__PURE__ */ Symbol.for("@piwitests/reporter:playwright-pause");
7365
+ function playwrightPauseOf(page) {
7366
+ const current = page.pause;
7367
+ if (typeof current !== "function") return null;
7368
+ const own = current[PLAYWRIGHT_PAUSE] ?? current;
7369
+ const proto = Object.getPrototypeOf(page);
7370
+ const theirs = proto?.pause;
7371
+ if (proto && typeof theirs === "function") {
7372
+ if (!theirs[PLAYWRIGHT_PAUSE]) {
7373
+ const wrapped = function(options) {
7374
+ return leaveToPlaywright(pageContext(this), () => theirs.call(this, options));
7375
+ };
7376
+ wrapped[PLAYWRIGHT_PAUSE] = theirs;
7377
+ proto.pause = wrapped;
7378
+ }
7379
+ markPausesWrapped();
6735
7380
  }
7381
+ return (options) => own.call(page, options);
6736
7382
  }
6737
7383
  function wrapLocator(page, locator, originMethod, originArgs) {
6738
7384
  return new Proxy(locator, {
@@ -6755,36 +7401,35 @@ function wrapLocator(page, locator, originMethod, originArgs) {
6755
7401
  const isNot = Boolean(callArgs[1]?.isNot);
6756
7402
  const callerLocation = sink ? captureCallerLocation() : null;
6757
7403
  if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
6758
- if (sink?.pause && pausesAt(sink.pause, callerLocation)) {
6759
- await pauseBefore(sink, page, target, callerLocation, assertionLabel(expression, isNot), {
7404
+ const call = knowLocatorCall(sink, page, target, callerLocation, { method: originMethod, args: originArgs });
7405
+ try {
7406
+ if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
7407
+ return await callMethod(target, prop, callArgs);
7408
+ }
7409
+ sink.lastActivePage = page;
7410
+ const used = {
6760
7411
  method: originMethod,
6761
- args: originArgs
6762
- });
6763
- }
6764
- if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
6765
- return callMethod(target, prop, callArgs);
6766
- }
6767
- sink.lastActivePage = page;
6768
- const used = {
6769
- method: originMethod,
6770
- args: originArgs,
6771
- raw: `${originMethod}(${JSON.stringify(originArgs)})`
6772
- };
6773
- const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
6774
- let seq = -1;
6775
- if (!alreadyProbed) {
6776
- seq = sink.capturedLocators.length;
6777
- sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
6778
- }
6779
- const result = await callMethod(target, prop, callArgs);
6780
- const matches = result?.matches;
6781
- if (matches === true && !alreadyProbed) {
6782
- if (callerLocation) sink.probedLocations.add(callerLocation);
6783
- startElementCapture(sink, page, target, seq, callerLocation, used);
6784
- } else if (matches === false) {
6785
- sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
7412
+ args: originArgs,
7413
+ raw: `${originMethod}(${JSON.stringify(originArgs)})`
7414
+ };
7415
+ const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
7416
+ let seq = -1;
7417
+ if (!alreadyProbed) {
7418
+ seq = sink.capturedLocators.length;
7419
+ sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
7420
+ }
7421
+ const result = await callMethod(target, prop, callArgs);
7422
+ const matches = result?.matches;
7423
+ if (matches === true && !alreadyProbed && !call?.picked) {
7424
+ if (callerLocation) sink.probedLocations.add(callerLocation);
7425
+ startElementCapture(sink, page, target, seq, callerLocation, used);
7426
+ } else if (matches === false) {
7427
+ sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
7428
+ }
7429
+ return result;
7430
+ } finally {
7431
+ forgetLocatorCall(sink, call);
6786
7432
  }
6787
- return result;
6788
7433
  };
6789
7434
  }
6790
7435
  if (!ACTION_METHOD_SET.has(prop)) return original;
@@ -6799,9 +7444,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
6799
7444
  args: originArgs,
6800
7445
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
6801
7446
  };
6802
- if (sink.pause && pausesAt(sink.pause, callerLocation)) {
6803
- await pauseBefore(sink, page, target, callerLocation, String(prop), used);
6804
- }
7447
+ const call = knowLocatorCall(sink, page, target, callerLocation, used);
6805
7448
  const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
6806
7449
  let seq = -1;
6807
7450
  if (!alreadyProbed) {
@@ -6819,8 +7462,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
6819
7462
  } catch (error) {
6820
7463
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
6821
7464
  throw error;
7465
+ } finally {
7466
+ forgetLocatorCall(sink, call);
6822
7467
  }
6823
- if (!alreadyProbed) {
7468
+ if (!alreadyProbed && !call?.picked) {
6824
7469
  if (callerLocation) sink.probedLocations.add(callerLocation);
6825
7470
  startElementCapture(sink, page, target, seq, callerLocation, used);
6826
7471
  }
@@ -6873,12 +7518,17 @@ function instrumentPage(page) {
6873
7518
  }
6874
7519
  const ctx = pageContext(page);
6875
7520
  if (ctx) instrumentContext(ctx);
7521
+ const playwrightPause = playwrightPauseOf(page);
7522
+ if (playwrightPause) {
7523
+ page.pause = () => pauseInPlace(page, playwrightPause, captureCallerLocation());
7524
+ }
6876
7525
  if (typeof page.close === "function") {
6877
7526
  const originalClose = page.close.bind(page);
6878
7527
  page.close = async (...args) => {
6879
7528
  await drainPendingProbes(1e3);
6880
7529
  const sink = currentSink;
6881
7530
  if (sink) {
7531
+ await endShownStops(sink, "closed", { page });
6882
7532
  await stashPageState(sink, { page });
6883
7533
  await maybeOpenPicker(sink, { page });
6884
7534
  await stopSinkCodeReach(sink, { page });
@@ -6967,8 +7617,18 @@ function instrumentPage(page) {
6967
7617
  } catch {
6968
7618
  }
6969
7619
  });
7620
+ page.on("request", (request) => {
7621
+ const sink = currentSink;
7622
+ if (!sink || sink.released.length === 0) return;
7623
+ sink.releasedRequests.add(request);
7624
+ sink.releasedQuietSince = Date.now();
7625
+ });
6970
7626
  const onRequestEnd = (request, failed) => {
6971
7627
  const sink = currentSink;
7628
+ if (sink?.releasedRequests.delete(request)) {
7629
+ sink.releasedQuietSince = Date.now();
7630
+ if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
7631
+ }
6972
7632
  if (!sink || RECORDED_OPEN.has(request)) return;
6973
7633
  sink.lastActivePage = page;
6974
7634
  const response = sink.openResponses.get(request) ?? null;
@@ -7074,6 +7734,10 @@ async function awaitFlakeConditions() {
7074
7734
  function instrumentContext(context) {
7075
7735
  if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
7076
7736
  INSTRUMENTED_CONTEXTS.add(context);
7737
+ if (typeof context.once === "function") {
7738
+ OPEN_CONTEXTS.add(context);
7739
+ context.once("close", () => OPEN_CONTEXTS.delete(context));
7740
+ }
7077
7741
  observeOwnApiRequests(context);
7078
7742
  if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
7079
7743
  void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
@@ -7093,6 +7757,7 @@ function instrumentContext(context) {
7093
7757
  await drainPendingProbes(1e3);
7094
7758
  const sink = currentSink;
7095
7759
  if (sink) {
7760
+ await endShownStops(sink, "closed", { context });
7096
7761
  await stashPageState(sink, { context });
7097
7762
  await maybeOpenPicker(sink, { context });
7098
7763
  await stopSinkCodeReach(sink, { context });
@@ -7109,6 +7774,7 @@ function patchBrowser(browser) {
7109
7774
  browser.newPage = async (...args) => {
7110
7775
  const page = await originalNewPage(...args);
7111
7776
  instrumentPage(page);
7777
+ await armBreakpoints([pageContext(page)]);
7112
7778
  await awaitFlakeConditions();
7113
7779
  await maybeStartCodeReach(page);
7114
7780
  return page;
@@ -7117,6 +7783,7 @@ function patchBrowser(browser) {
7117
7783
  browser.newContext = async (...args) => {
7118
7784
  const context = await originalNewContext(...args);
7119
7785
  instrumentContext(context);
7786
+ await armBreakpoints([context]);
7120
7787
  return context;
7121
7788
  };
7122
7789
  if (typeof browser.close === "function") {
@@ -7168,14 +7835,20 @@ async function flushSink(sink, testInfo) {
7168
7835
  let drainDeadline;
7169
7836
  await Promise.race([
7170
7837
  Promise.allSettled(sink.capturePromises),
7171
- new Promise((resolve3) => {
7172
- drainDeadline = setTimeout(resolve3, 2e3);
7838
+ new Promise((resolve5) => {
7839
+ drainDeadline = setTimeout(resolve5, 2e3);
7173
7840
  })
7174
7841
  ]);
7175
7842
  clearTimeout(drainDeadline);
7176
7843
  const page = sink.lastActivePage;
7177
7844
  const pageReadable = page !== null && !isPageClosed(page);
7178
7845
  if (pageReadable) await maybeOpenPicker(sink);
7846
+ if (sink.stops.length > 0) {
7847
+ await testInfo.attach(ATTACHMENT_NAMES.pauses, {
7848
+ contentType: "application/json",
7849
+ body: Buffer.from(JSON.stringify(sink.stops))
7850
+ });
7851
+ }
7179
7852
  if (sink.capturedLocators.length > 0) {
7180
7853
  await testInfo.attach(ATTACHMENT_NAMES.locators, {
7181
7854
  contentType: "application/json",
@@ -7376,6 +8049,7 @@ var piwiFixtures = {
7376
8049
  // never double-wraps a page the browser patch already instrumented.
7377
8050
  page: async ({ page }, use) => {
7378
8051
  instrumentPage(page);
8052
+ await armBreakpoints([pageContext(page)]);
7379
8053
  await awaitFlakeConditions();
7380
8054
  await maybeStartCodeReach(page);
7381
8055
  await use(page);
@@ -7396,8 +8070,9 @@ var piwiFixtures = {
7396
8070
  if (pauseSet.size) {
7397
8071
  const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
7398
8072
  if (reason) logPauseSkipOnce(reason);
7399
- else sink.pause = { set: pauseSet, next: false, finished: false };
8073
+ else sink.pause = { set: pauseSet, step: false, finished: false };
7400
8074
  }
8075
+ if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
7401
8076
  if (isFlakeMode()) {
7402
8077
  const plan = loadFlakePlan();
7403
8078
  const role = flakeRoleForTest(plan, {
@@ -7422,9 +8097,12 @@ var piwiFixtures = {
7422
8097
  activeResourceLedger()?.testStarted(testInfo);
7423
8098
  currentSink = sink;
7424
8099
  try {
8100
+ await armBreakpoints(OPEN_CONTEXTS);
7425
8101
  await startCodeReachOnOpenPages();
7426
8102
  await use();
7427
8103
  } finally {
8104
+ await endShownStops(sink, "ended");
8105
+ await awaitReleasedCalls(sink);
7428
8106
  currentSink = null;
7429
8107
  if (sink.probeItem) {
7430
8108
  const applied = sink.probeInterception?.applied() ?? false;