@piwitests/reporter 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -517,8 +517,8 @@ function installPickerOverlay(arg) {
517
517
  return root && root !== doc && root.host ? root.host : null;
518
518
  };
519
519
  const targetOf = (e) => {
520
- const path13 = typeof e.composedPath === "function" ? e.composedPath() : null;
521
- const first = path13 && path13.length ? path13[0] : null;
520
+ const path15 = typeof e.composedPath === "function" ? e.composedPath() : null;
521
+ const first = path15 && path15.length ? path15[0] : null;
522
522
  return first && first.nodeType === 1 ? first : e.target;
523
523
  };
524
524
  const buildChain = (raw) => {
@@ -1222,8 +1222,11 @@ function showPauseBar(arg) {
1222
1222
  };
1223
1223
  const buttons = [
1224
1224
  ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
1225
- ["Step", "step", "Pause again at the next action", false],
1225
+ ["Step", "step", "Pause again before the next call", false],
1226
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
+ ] : [],
1227
1230
  ["Finish", "finish", "Run on and pause no more in this test", false]
1228
1231
  ];
1229
1232
  for (const [label, choice, hint, primary] of buttons) {
@@ -1985,21 +1988,21 @@ var MAX_INDETERMINATE = 20;
1985
1988
  var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
1986
1989
  var LEVEL = `${STEP}(?:/${STEP})*`;
1987
1990
  var ELEMENT_PATH_RE = new RegExp(`^${LEVEL}(?:\\|${LEVEL})*$`);
1988
- function isElementPath(path13) {
1989
- 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);
1990
1993
  }
1991
1994
  var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
1992
1995
  function elementRef(v) {
1993
1996
  if (!v || typeof v !== "object") return null;
1994
- const { path: path13, label } = v;
1995
- if (!isElementPath(path13)) return null;
1996
- 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) : "" };
1997
2000
  }
1998
2001
  function textPoint(v) {
1999
2002
  if (!v || typeof v !== "object") return null;
2000
- const { path: path13, offset } = v;
2003
+ const { path: path15, offset } = v;
2001
2004
  const at = offsetOf(offset);
2002
- return isElementPath(path13) && at !== null ? { path: path13, offset: at } : null;
2005
+ return isElementPath(path15) && at !== null ? { path: path15, offset: at } : null;
2003
2006
  }
2004
2007
  function sanitizePageInteraction(raw) {
2005
2008
  if (!raw || typeof raw !== "object") return {};
@@ -2405,7 +2408,8 @@ var ATTACHMENT_NAMES = {
2405
2408
  userPick: "piwi-user-pick",
2406
2409
  aiUsage: "piwi-ai-usage",
2407
2410
  aiMeta: "piwi-ai-meta",
2408
- resources: "piwi-resources"
2411
+ resources: "piwi-resources",
2412
+ pauses: "piwi-pauses"
2409
2413
  };
2410
2414
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
2411
2415
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
@@ -2639,13 +2643,19 @@ var LocatorPageLog = class {
2639
2643
  };
2640
2644
 
2641
2645
  // src/internal/probe/mode.ts
2642
- var fs2 = __toESM(require("fs"));
2646
+ var fs3 = __toESM(require("fs"));
2647
+
2648
+ // src/internal/config/env.ts
2649
+ var path3 = __toESM(require("path"));
2643
2650
 
2644
2651
  // src/internal/config/desktop.ts
2645
2652
  var fs = __toESM(require("fs"));
2646
2653
  var os = __toESM(require("os"));
2647
2654
  var path2 = __toESM(require("path"));
2648
2655
 
2656
+ // src/internal/config/dotenv.ts
2657
+ var fs2 = __toESM(require("fs"));
2658
+
2649
2659
  // src/internal/config/env.ts
2650
2660
  var DEFAULTS = {
2651
2661
  projectName: "default-project",
@@ -2682,6 +2692,7 @@ var DEFAULTS = {
2682
2692
  };
2683
2693
  var PIWI_ENV_KEYS = {
2684
2694
  serverUrl: "PIWI_DASHBOARD_URL",
2695
+ desktopApp: "PIWI_DESKTOP_APP",
2685
2696
  projectName: "PIWI_PROJECT_NAME",
2686
2697
  verbose: "PIWI_VERBOSE",
2687
2698
  apiKey: "PIWI_API_KEY",
@@ -2779,14 +2790,14 @@ var ENV_FALLBACK_SPECS = [
2779
2790
  ];
2780
2791
 
2781
2792
  // src/internal/support/test-paths.ts
2782
- var path3 = __toESM(require("path"));
2793
+ var path4 = __toESM(require("path"));
2783
2794
  function configDirFromConfig(config) {
2784
- if (config?.configFile) return path3.dirname(config.configFile);
2795
+ if (config?.configFile) return path4.dirname(config.configFile);
2785
2796
  if (config?.rootDir) return config.rootDir;
2786
2797
  return process.cwd();
2787
2798
  }
2788
2799
  function relativeTestPath(file, base) {
2789
- return path3.relative(base, file).split(path3.sep).join("/");
2800
+ return path4.relative(base, file).split(path4.sep).join("/");
2790
2801
  }
2791
2802
 
2792
2803
  // src/internal/probe/plan.ts
@@ -2796,8 +2807,8 @@ function parseRouteKey(routeKey2) {
2796
2807
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
2797
2808
  }
2798
2809
  function patternToRegex(pattern) {
2799
- const path13 = pattern.split("?")[0] ?? pattern;
2800
- 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, "[^/]+");
2801
2812
  return new RegExp(`^${escaped}$`);
2802
2813
  }
2803
2814
  function suitePathEqual(a, b) {
@@ -2820,13 +2831,13 @@ function matchProbeItem(plan, test) {
2820
2831
  function requestMatchesRoute(method, url, routeKey2) {
2821
2832
  const { method: rm, pattern } = parseRouteKey(routeKey2);
2822
2833
  if (rm && rm !== method.toUpperCase()) return false;
2823
- let path13;
2834
+ let path15;
2824
2835
  try {
2825
- path13 = new URL(url).pathname;
2836
+ path15 = new URL(url).pathname;
2826
2837
  } catch {
2827
- path13 = url.split("?")[0] ?? url;
2838
+ path15 = url.split("?")[0] ?? url;
2828
2839
  }
2829
- return patternToRegex(pattern).test(path13);
2840
+ return patternToRegex(pattern).test(path15);
2830
2841
  }
2831
2842
  function createProbeState() {
2832
2843
  return { navigated: false, matches: 0 };
@@ -2848,10 +2859,10 @@ function isProbeMode() {
2848
2859
  function loadPlan() {
2849
2860
  if (planCache !== void 0) return planCache;
2850
2861
  planCache = null;
2851
- const path13 = process.env[PIWI_PROBE_ENV.plan];
2852
- if (path13) {
2862
+ const path15 = process.env[PIWI_PROBE_ENV.plan];
2863
+ if (path15) {
2853
2864
  try {
2854
- planCache = JSON.parse(fs2.readFileSync(path13, "utf8"));
2865
+ planCache = JSON.parse(fs3.readFileSync(path15, "utf8"));
2855
2866
  } catch {
2856
2867
  planCache = null;
2857
2868
  }
@@ -2875,10 +2886,10 @@ function outcomeFromStatus(status, applied) {
2875
2886
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2876
2887
  }
2877
2888
  function recordProbeOutcome(line) {
2878
- const path13 = process.env[PIWI_PROBE_ENV.results];
2879
- if (!path13) return;
2889
+ const path15 = process.env[PIWI_PROBE_ENV.results];
2890
+ if (!path15) return;
2880
2891
  try {
2881
- fs2.appendFileSync(path13, `${JSON.stringify(line)}
2892
+ fs3.appendFileSync(path15, `${JSON.stringify(line)}
2882
2893
  `);
2883
2894
  } catch {
2884
2895
  }
@@ -3008,7 +3019,7 @@ function originOf(url) {
3008
3019
  return null;
3009
3020
  }
3010
3021
  }
3011
- 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)));
3012
3023
  async function performRouteAction(route, action, startedAt) {
3013
3024
  switch (action.kind) {
3014
3025
  case "abort":
@@ -3119,7 +3130,7 @@ async function installProbeInterception(page, item) {
3119
3130
  }
3120
3131
 
3121
3132
  // src/internal/flake/mode.ts
3122
- var fs3 = __toESM(require("fs"));
3133
+ var fs4 = __toESM(require("fs"));
3123
3134
 
3124
3135
  // ../core/src/error-parse.ts
3125
3136
  var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
@@ -3430,7 +3441,7 @@ function loadFlakePlan() {
3430
3441
  if (planCache2 && planCache2.file === file) return planCache2.plan;
3431
3442
  let text;
3432
3443
  try {
3433
- text = fs3.readFileSync(file, "utf8");
3444
+ text = fs4.readFileSync(file, "utf8");
3434
3445
  } catch (error) {
3435
3446
  throw new Error(`[Piwi Dashboard] Cannot read the flake plan ${file}: ${errorMessage(error)}`);
3436
3447
  }
@@ -3583,7 +3594,7 @@ function recordFlakeResult(line) {
3583
3594
  const file = process.env[PIWI_FLAKE_ENV.results]?.trim();
3584
3595
  if (!file) return;
3585
3596
  try {
3586
- fs3.appendFileSync(file, `${JSON.stringify(line)}
3597
+ fs4.appendFileSync(file, `${JSON.stringify(line)}
3587
3598
  `);
3588
3599
  } catch {
3589
3600
  }
@@ -3649,13 +3660,13 @@ function normalizeFunctionRanges(ranges) {
3649
3660
  }
3650
3661
  function normalizeCoveragePath(raw) {
3651
3662
  if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
3652
- const path13 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
3653
- if (!path13 || path13.startsWith("../") || path13.startsWith("/") || /^[A-Za-z]:\//.test(path13)) return null;
3654
- if (/(^|\/)node_modules\//.test(path13)) return null;
3655
- 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;
3656
3667
  }
3657
- function fileEntry(path13, runtime, blob, functions = [], lines = []) {
3658
- const file = { path: path13, runtime };
3668
+ function fileEntry(path15, runtime, blob, functions = [], lines = []) {
3669
+ const file = { path: path15, runtime };
3659
3670
  if (blob && (functions.length > 0 || lines.length > 0)) file.blob = blob;
3660
3671
  if (functions.length > 0) file.functions = functions;
3661
3672
  if (lines.length > 0) file.lines = lines;
@@ -3674,11 +3685,11 @@ function sanitizeCoverage(raw) {
3674
3685
  const files = [];
3675
3686
  for (const item of raw.files) {
3676
3687
  if (!isRecord(item) || !isRuntime(item.runtime)) continue;
3677
- const path13 = normalizeCoveragePath(item.path);
3678
- if (path13 === null) continue;
3688
+ const path15 = normalizeCoveragePath(item.path);
3689
+ if (path15 === null) continue;
3679
3690
  const blob = typeof item.blob === "string" && BLOB_ID.test(item.blob) ? item.blob : void 0;
3680
3691
  files.push(
3681
- fileEntry(path13, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
3692
+ fileEntry(path15, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
3682
3693
  );
3683
3694
  }
3684
3695
  const merged = mergeCoverage(captureOf(files, raw.incomplete === true));
@@ -4089,7 +4100,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
4089
4100
  }
4090
4101
 
4091
4102
  // src/internal/capture/pick-on-failure.ts
4092
- var path4 = __toESM(require("path"));
4103
+ var path5 = __toESM(require("path"));
4093
4104
  function parseLeafLocatorExpression(rawExpr) {
4094
4105
  const leaf = parseLeafLocatorCall(rawExpr);
4095
4106
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -4104,37 +4115,60 @@ ${err.stack ?? ""}`));
4104
4115
  const parsed2 = parseLeafLocatorExpression(header);
4105
4116
  if (!parsed2) continue;
4106
4117
  const loc = err.location;
4107
- 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;
4108
4119
  return { method: parsed2.method, args: parsed2.args, location };
4109
4120
  }
4110
4121
  return null;
4111
4122
  }
4112
- async function cleanupPicker(page) {
4123
+ var PICK_ENDED = "__piwiPickEnded";
4124
+ async function cleanupPicker(page, ended = false) {
4113
4125
  try {
4114
- await page.evaluate(() => {
4115
- const g = globalThis;
4116
- if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
4117
- if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
4118
- delete g.__piwiPickCleanup;
4119
- delete g.__piwiAnchorCleanup;
4120
- delete g.__piwiPickState;
4121
- delete g.__piwiPickChoice;
4122
- delete g.__piwiPickedElement;
4123
- delete g.__piwiAnchorState;
4124
- delete g.__piwiPickAnchors;
4125
- delete g.__piwiPickChainCount;
4126
- });
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
+ );
4127
4144
  } catch {
4128
4145
  }
4129
4146
  }
4130
- 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
+ };
4131
4167
  try {
4132
4168
  const overlayArg = { transport: "global", failing };
4169
+ await page.evaluate((endedKey) => delete globalThis[endedKey], PICK_ENDED);
4133
4170
  await page.evaluate(installPickerOverlay, overlayArg);
4134
- await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
4135
- timeout: 0,
4136
- polling: 250
4137
- });
4171
+ if (!await answered("__piwiPickState")) return null;
4138
4172
  const state = await page.evaluate(() => globalThis.__piwiPickState);
4139
4173
  if (state !== "picked") return null;
4140
4174
  const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
@@ -4155,10 +4189,7 @@ async function pickLocator(page, probe, failing) {
4155
4189
  leafLevel: level,
4156
4190
  leafTestId: attrs.attributes["data-testid"] ?? null
4157
4191
  });
4158
- await page.waitForFunction(() => globalThis.__piwiAnchorState !== void 0, void 0, {
4159
- timeout: 0,
4160
- polling: 250
4161
- });
4192
+ if (!await answered("__piwiAnchorState")) return null;
4162
4193
  const anchorResult = await page.evaluate(() => {
4163
4194
  const g = globalThis;
4164
4195
  return { state: g.__piwiAnchorState, anchors: g.__piwiPickAnchors ?? [], chainCount: g.__piwiPickChainCount };
@@ -4180,10 +4211,7 @@ async function pickLocator(page, probe, failing) {
4180
4211
  failing,
4181
4212
  choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
4182
4213
  });
4183
- await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
4184
- timeout: 0,
4185
- polling: 250
4186
- });
4214
+ if (!await answered("__piwiPickChoice")) return null;
4187
4215
  const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
4188
4216
  if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
4189
4217
  const picked = { ...ranked[choice], pickedByUser: true };
@@ -4202,7 +4230,7 @@ async function pickLocator(page, probe, failing) {
4202
4230
  ...anchors.length > 0 ? { anchors } : {}
4203
4231
  };
4204
4232
  } finally {
4205
- await cleanupPicker(page);
4233
+ await cleanupPicker(page, ended?.aborted);
4206
4234
  }
4207
4235
  }
4208
4236
  async function runLocatorPicker(page, testInfo, failed, probe) {
@@ -4257,7 +4285,7 @@ function applyPickToSnapshots(snapshots, pick) {
4257
4285
  }
4258
4286
 
4259
4287
  // src/internal/capture/pause-at.ts
4260
- var path5 = __toESM(require("path"));
4288
+ var path8 = __toESM(require("path"));
4261
4289
 
4262
4290
  // ../core/src/editor-send.ts
4263
4291
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
@@ -4283,6 +4311,93 @@ function isInsidePath(file) {
4283
4311
  return file.split(/[\\/]/).every((part) => part !== "..");
4284
4312
  }
4285
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
+
4286
4401
  // src/internal/capture/pause-at.ts
4287
4402
  var PauseSet = class {
4288
4403
  constructor(lines) {
@@ -4298,18 +4413,32 @@ var PauseSet = class {
4298
4413
  return col > 0 && this.lines.has(callerLocation.slice(0, col));
4299
4414
  }
4300
4415
  };
4301
- var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
4416
+ var NO_BREAKPOINTS = new PauseSet(/* @__PURE__ */ new Set());
4302
4417
  function callSiteFile(file, cwd) {
4303
4418
  const slashed = file.replace(/\\/g, "/");
4304
- const absolute = path5.isAbsolute(file) || path5.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
4305
- let rel = absolute ? path5.relative(cwd, file) : path5.normalize(slashed);
4306
- 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, "/");
4307
4422
  if (rel.startsWith("./")) rel = rel.slice(2);
4308
4423
  return rel;
4309
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
+ }
4310
4439
  var ignoredLogged = /* @__PURE__ */ new Set();
4311
4440
  function parsePauseAt(value, cwd = process.cwd()) {
4312
- if (!value?.trim()) return EMPTY;
4441
+ if (!value?.trim()) return NO_BREAKPOINTS;
4313
4442
  const lines = /* @__PURE__ */ new Set();
4314
4443
  for (const raw of value.split(";")) {
4315
4444
  const entry = raw.trim();
@@ -4325,7 +4454,7 @@ function parsePauseAt(value, cwd = process.cwd()) {
4325
4454
  }
4326
4455
  lines.add(`${callSiteFile(m[1], cwd)}:${line}`);
4327
4456
  }
4328
- return lines.size ? new PauseSet(lines) : EMPTY;
4457
+ return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
4329
4458
  }
4330
4459
  var parsed = null;
4331
4460
  function currentPauseSet() {
@@ -4335,27 +4464,121 @@ function currentPauseSet() {
4335
4464
  }
4336
4465
  function pauseGateFromTestInfo(testInfo) {
4337
4466
  const use = testInfo.project?.use ?? {};
4338
- 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 };
4339
4469
  }
4340
4470
  function pauseSkipReason(gate) {
4341
4471
  if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
4342
4472
  if (gate.headless !== false)
4343
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`;
4344
4478
  return null;
4345
4479
  }
4346
- function rearmedTimeout(before, pausedMs) {
4347
- 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;
4348
4577
  }
4349
4578
  function pausePlace(callerLocation) {
4350
- const col = callerLocation.lastIndexOf(":");
4351
- const fileLine = col > 0 ? callerLocation.slice(0, col) : callerLocation;
4579
+ const fileLine = callSiteLine(callerLocation);
4352
4580
  return fileLine.slice(fileLine.lastIndexOf("/") + 1);
4353
4581
  }
4354
- function assertionLabel(expression, isNot) {
4355
- const [first = "", ...rest] = expression.split(".").filter(Boolean);
4356
- const name = first + rest.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
4357
- return `${isNot ? "not." : ""}${name || "expect"}`;
4358
- }
4359
4582
  function pickPlace(callerLocation) {
4360
4583
  const m = /^(.+):(\d+):\d+$/.exec(callerLocation);
4361
4584
  if (!m) return null;
@@ -4400,59 +4623,312 @@ function logPauseSkipOnce(reason) {
4400
4623
  skipLogged = true;
4401
4624
  console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt} is set but breakpoints are skipped: ${reason}`);
4402
4625
  }
4626
+ var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
4627
+ var MAX_NAVIGATED = 10;
4403
4628
  var HIGHLIGHT_MS = 2e3;
4404
- async function pauseHere(page, testInfo, point, probe, onPick) {
4405
- const before = testInfo.timeout;
4406
- const started = Date.now();
4629
+ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
4407
4630
  let shown = null;
4408
4631
  try {
4409
- testInfo.setTimeout(0);
4410
4632
  const highlight = point.target?.highlight;
4411
4633
  if (typeof highlight === "function") {
4412
4634
  let timer;
4413
4635
  shown = await Promise.race([
4414
4636
  Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
4415
- new Promise((resolve3) => {
4416
- timer = setTimeout(() => resolve3(null), HIGHLIGHT_MS);
4637
+ new Promise((resolve5) => {
4638
+ timer = setTimeout(() => resolve5(null), HIGHLIGHT_MS);
4417
4639
  })
4418
4640
  ]);
4419
4641
  clearTimeout(timer);
4420
4642
  }
4421
4643
  const arg = {
4422
4644
  place: pausePlace(point.location),
4423
- attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
4424
- action: point.action,
4425
- locator: point.locator
4645
+ attempt: point.attempt,
4646
+ action: point.call,
4647
+ locator: null,
4648
+ record: !!onRecord
4426
4649
  };
4427
4650
  console.log(
4428
- `[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).`
4429
4652
  );
4653
+ let navigated = 0;
4430
4654
  for (; ; ) {
4431
- await page.evaluate(showPauseBar, arg);
4432
- await page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 });
4433
- const choice = await page.evaluate(takePauseState);
4434
- if (choice === "resume" || choice === "step" || choice === "finish") return choice;
4435
- if (choice !== "pick") continue;
4436
- const picked = await pickLocator(page, probe, null).catch(() => null);
4437
- 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
+ }
4438
4677
  }
4439
4678
  } catch {
4440
4679
  return "resume";
4441
4680
  } finally {
4442
4681
  try {
4682
+ if (ended?.aborted) await page.evaluate(takePauseState);
4443
4683
  const dispose = shown?.dispose;
4444
4684
  const hide = page.hideHighlight;
4445
4685
  if (typeof dispose === "function") await dispose.call(shown);
4446
4686
  else if (typeof hide === "function") await hide.call(page);
4447
4687
  } catch {
4448
4688
  }
4449
- try {
4450
- testInfo.setTimeout(rearmedTimeout(before, Date.now() - started));
4451
- } catch {
4452
- }
4453
4689
  }
4454
4690
  }
4455
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
+
4456
4932
  // src/internal/collect/coverage-settings.ts
4457
4933
  var SERVER_VALUES = /* @__PURE__ */ new Set(["files", "functions", "lines"]);
4458
4934
  function isServerGranularity(value) {
@@ -4468,7 +4944,7 @@ function browserFunctionsInWorker() {
4468
4944
  function browserFunctionCoverage(files) {
4469
4945
  return sanitizeCoverage({
4470
4946
  v: 1,
4471
- files: [...files].map(([path13, functions]) => ({ path: path13, runtime: "browser", functions }))
4947
+ files: [...files].map(([path15, functions]) => ({ path: path15, runtime: "browser", functions }))
4472
4948
  });
4473
4949
  }
4474
4950
  function newKnownFunctions(known, sent) {
@@ -4481,19 +4957,19 @@ function newKnownFunctions(known, sent) {
4481
4957
  budget = MAX_COVERAGE_RANGES;
4482
4958
  };
4483
4959
  for (const [raw, ranges] of known) {
4484
- const path13 = normalizeCoveragePath(raw);
4485
- if (path13 === null) continue;
4486
- let named = sent.get(path13);
4960
+ const path15 = normalizeCoveragePath(raw);
4961
+ if (path15 === null) continue;
4962
+ let named = sent.get(path15);
4487
4963
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
4488
4964
  if (fresh.length === 0) continue;
4489
- if (!named) sent.set(path13, named = /* @__PURE__ */ new Set());
4965
+ if (!named) sent.set(path15, named = /* @__PURE__ */ new Set());
4490
4966
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
4491
4967
  while (fresh.length > 0) {
4492
4968
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
4493
4969
  const part = fresh.slice(0, budget);
4494
4970
  fresh = fresh.slice(part.length);
4495
4971
  budget -= part.length;
4496
- files.push({ path: path13, allFunctions: part });
4972
+ files.push({ path: path15, allFunctions: part });
4497
4973
  }
4498
4974
  }
4499
4975
  close();
@@ -4504,9 +4980,9 @@ function serverCoverageInWorker() {
4504
4980
  }
4505
4981
 
4506
4982
  // src/internal/support/aria-sampling.ts
4507
- var path6 = __toESM(require("path"));
4983
+ var path10 = __toESM(require("path"));
4508
4984
  var os3 = __toESM(require("os"));
4509
- var fs4 = __toESM(require("fs"));
4985
+ var fs7 = __toESM(require("fs"));
4510
4986
 
4511
4987
  // src/internal/support/instance-id.ts
4512
4988
  var crypto = __toESM(require("crypto"));
@@ -4520,14 +4996,14 @@ function ariaSampleIdentity(filePath, title) {
4520
4996
  return `${filePath}\0${title}`;
4521
4997
  }
4522
4998
  function getAriaSampleFilePath(projectName) {
4523
- 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`);
4524
5000
  }
4525
5001
  var cachedSets = /* @__PURE__ */ new Map();
4526
5002
  function loadAriaSampleSet(projectName) {
4527
5003
  if (cachedSets.has(projectName)) return cachedSets.get(projectName);
4528
5004
  let set = null;
4529
5005
  try {
4530
- const raw = fs4.readFileSync(getAriaSampleFilePath(projectName), "utf8");
5006
+ const raw = fs7.readFileSync(getAriaSampleFilePath(projectName), "utf8");
4531
5007
  const parsed2 = JSON.parse(raw);
4532
5008
  if (parsed2.projectName === projectName && Array.isArray(parsed2.identities)) {
4533
5009
  set = new Set(parsed2.identities.filter((x) => typeof x === "string"));
@@ -4548,97 +5024,10 @@ function isDueForAriaSample(testInfo) {
4548
5024
  );
4549
5025
  }
4550
5026
 
4551
- // src/internal/capture/quiet-capture.ts
4552
- var import_node_module = require("module");
4553
- var path8 = __toESM(require("path"));
4554
-
4555
- // src/internal/support/reporter-version.ts
4556
- var fs5 = __toESM(require("fs"));
4557
- var path7 = __toESM(require("path"));
4558
- var CANDIDATE_OFFSETS = ["..", "../../.."];
4559
- function findOwnPackageJson(fromDir) {
4560
- for (const offset of CANDIDATE_OFFSETS) {
4561
- try {
4562
- const pkgPath = path7.resolve(fromDir, offset, "package.json");
4563
- const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
4564
- const { name, version } = pkg;
4565
- if (name === "@piwitests/reporter" && typeof version === "string")
4566
- return { version, root: path7.dirname(pkgPath) };
4567
- } catch {
4568
- }
4569
- }
4570
- return void 0;
4571
- }
4572
-
4573
- // src/internal/capture/quiet-capture.ts
4574
- var BOXED_PREFIX_EXPORTS = [
4575
- [
4576
- "playwright-core/lib/coreBundle",
4577
- (mod) => mod?.utils?.setBoxedStackPrefixes
4578
- ],
4579
- ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
4580
- ];
4581
- var boxing;
4582
- var resolvedPlaywrightPackageJson;
4583
- function resolvePlaywrightPackageJson() {
4584
- if (resolvedPlaywrightPackageJson !== void 0) return resolvedPlaywrightPackageJson;
4585
- try {
4586
- const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
4587
- resolvedPlaywrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
4588
- } catch {
4589
- resolvedPlaywrightPackageJson = null;
4590
- }
4591
- return resolvedPlaywrightPackageJson;
4592
- }
4593
- function resolveBoxing() {
4594
- try {
4595
- const root = findOwnPackageJson(__dirname)?.root;
4596
- if (!root) return null;
4597
- const playwrightPackageJson = resolvePlaywrightPackageJson();
4598
- if (!playwrightPackageJson) return null;
4599
- const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
4600
- for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
4601
- let set;
4602
- try {
4603
- set = pick(requireFromPlaywright(moduleId));
4604
- } catch {
4605
- continue;
4606
- }
4607
- if (typeof set !== "function") continue;
4608
- return {
4609
- set,
4610
- prefixes: [path8.dirname(playwrightPackageJson), path8.join(root, "dist") + path8.sep]
4611
- };
4612
- }
4613
- } catch {
4614
- }
4615
- return null;
4616
- }
4617
- function boxCaptureFrames(extraPrefixes = []) {
4618
- const first = boxing === void 0;
4619
- if (first) boxing = resolveBoxing();
4620
- if (!boxing) return;
4621
- const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
4622
- if (!first && added.length === 0) return;
4623
- boxing.prefixes.push(...added);
4624
- try {
4625
- boxing.set([...boxing.prefixes]);
4626
- } catch {
4627
- }
4628
- }
4629
- function callMethod(target, prop, args) {
4630
- return target[prop](...args);
4631
- }
4632
- function internalCall(owner, run) {
4633
- const wrap = owner?._wrapApiCall;
4634
- if (typeof wrap !== "function") return run();
4635
- return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
4636
- }
4637
-
4638
5027
  // src/internal/capture/code-reach.ts
4639
5028
  var crypto2 = __toESM(require("crypto"));
4640
- var fs6 = __toESM(require("fs"));
4641
- var path9 = __toESM(require("path"));
5029
+ var fs8 = __toESM(require("fs"));
5030
+ var path11 = __toESM(require("path"));
4642
5031
  var import_node_child_process = require("child_process");
4643
5032
  var MAX_MAP_BYTES = 20 * 1024 * 1024;
4644
5033
  var MAP_TIMEOUT_MS = 5e3;
@@ -4681,18 +5070,18 @@ function repositoryRoot(from) {
4681
5070
  }).trim();
4682
5071
  } catch {
4683
5072
  }
4684
- cachedRepoRoot = { from, root: path9.resolve(root) };
5073
+ cachedRepoRoot = { from, root: path11.resolve(root) };
4685
5074
  return cachedRepoRoot.root;
4686
5075
  }
4687
5076
  function codeReachRoots(configDir, configured) {
4688
5077
  const repoRoot = repositoryRoot(configDir);
4689
- const resolve3 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path9.resolve(configDir, r));
4690
- 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?.["*"]);
4691
5080
  const byOrigin = /* @__PURE__ */ new Map();
4692
5081
  if (configured && !Array.isArray(configured)) {
4693
5082
  for (const [key, dirs] of Object.entries(configured)) {
4694
5083
  const origin = key === "*" ? null : originOf2(key);
4695
- const resolved = resolve3(dirs);
5084
+ const resolved = resolve5(dirs);
4696
5085
  if (origin && resolved.length) byOrigin.set(origin, resolved);
4697
5086
  }
4698
5087
  }
@@ -4715,7 +5104,7 @@ function scriptRoots(roots, scriptUrl, pageUrl) {
4715
5104
  }
4716
5105
  function isFile(file) {
4717
5106
  try {
4718
- return fs6.statSync(file).isFile();
5107
+ return fs8.statSync(file).isFile();
4719
5108
  } catch {
4720
5109
  return false;
4721
5110
  }
@@ -4723,10 +5112,10 @@ function isFile(file) {
4723
5112
  function resolveSourcePath(source, roots) {
4724
5113
  const normalized = normalizeSourcePath(source);
4725
5114
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
4726
- if (path9.isAbsolute(normalized) && isFile(normalized)) return path9.resolve(normalized);
5115
+ if (path11.isAbsolute(normalized) && isFile(normalized)) return path11.resolve(normalized);
4727
5116
  const relative7 = normalized.replace(/^\/+/, "");
4728
5117
  for (const root of roots) {
4729
- const candidate = path9.resolve(root, relative7);
5118
+ const candidate = path11.resolve(root, relative7);
4730
5119
  if (isFile(candidate)) return candidate;
4731
5120
  }
4732
5121
  return null;
@@ -4758,7 +5147,7 @@ function fileForUrl(url, roots, bases) {
4758
5147
  return file;
4759
5148
  }
4760
5149
  function relativeToRepo(file, repoRoot) {
4761
- return path9.relative(repoRoot, file).split(path9.sep).join("/");
5150
+ return path11.relative(repoRoot, file).split(path11.sep).join("/");
4762
5151
  }
4763
5152
  function scriptVersion(entry) {
4764
5153
  return entry.hash ?? crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -4821,10 +5210,10 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
4821
5210
  const files = /* @__PURE__ */ new Map();
4822
5211
  const known = /* @__PURE__ */ new Map();
4823
5212
  const addTo = (into, file, functions) => {
4824
- const path13 = relativeToRepo(file, roots.repoRoot);
4825
- const listed = into.get(path13);
5213
+ const path15 = relativeToRepo(file, roots.repoRoot);
5214
+ const listed = into.get(path15);
4826
5215
  if (listed) listed.push(...functions);
4827
- else into.set(path13, [...functions]);
5216
+ else into.set(path15, [...functions]);
4828
5217
  };
4829
5218
  const add = (file, functions) => addTo(files, file, functions);
4830
5219
  const bases = viteBases(entries);
@@ -4976,8 +5365,8 @@ function onScriptParsed(page, capture, event) {
4976
5365
  var READ_WAIT_MS = 5e3;
4977
5366
  function readCoverage(page, cdp) {
4978
5367
  let timer;
4979
- const late = new Promise((resolve3) => {
4980
- timer = setTimeout(() => resolve3([]), READ_WAIT_MS);
5368
+ const late = new Promise((resolve5) => {
5369
+ timer = setTimeout(() => resolve5([]), READ_WAIT_MS);
4981
5370
  });
4982
5371
  const taken = internalCall(page, () => cdp.send("Profiler.takePreciseCoverage")).then(
4983
5372
  (answer) => answer.result,
@@ -5087,20 +5476,20 @@ async function stopCodeReach(page) {
5087
5476
  }
5088
5477
 
5089
5478
  // src/internal/capture/resource-ledger.ts
5090
- var fs8 = __toESM(require("fs"));
5091
- var path11 = __toESM(require("path"));
5479
+ var fs10 = __toESM(require("fs"));
5480
+ var path13 = __toESM(require("path"));
5092
5481
  var import_node_module2 = require("module");
5093
5482
 
5094
5483
  // src/internal/capture/worker-metrics.ts
5095
5484
  var import_node_perf_hooks = require("perf_hooks");
5096
5485
 
5097
5486
  // src/internal/support/system-readers.ts
5098
- var fs7 = __toESM(require("fs"));
5099
- var path10 = __toESM(require("path"));
5487
+ var fs9 = __toESM(require("fs"));
5488
+ var path12 = __toESM(require("path"));
5100
5489
  var CLK_TCK = 100;
5101
5490
  function readText(file) {
5102
5491
  try {
5103
- return fs7.readFileSync(file, "utf8");
5492
+ return fs9.readFileSync(file, "utf8");
5104
5493
  } catch {
5105
5494
  return null;
5106
5495
  }
@@ -5182,45 +5571,45 @@ var ProcFs = class {
5182
5571
  }
5183
5572
  /** Whether this looks like a Linux `/proc` at all. */
5184
5573
  available() {
5185
- return fs7.existsSync(path10.join(this.root, "stat"));
5574
+ return fs9.existsSync(path12.join(this.root, "stat"));
5186
5575
  }
5187
5576
  /** Every process, keyed by pid. */
5188
5577
  table() {
5189
5578
  const out = /* @__PURE__ */ new Map();
5190
5579
  let names;
5191
5580
  try {
5192
- names = fs7.readdirSync(this.root);
5581
+ names = fs9.readdirSync(this.root);
5193
5582
  } catch {
5194
5583
  return out;
5195
5584
  }
5196
5585
  for (const name of names) {
5197
5586
  if (!/^\d+$/.test(name)) continue;
5198
- const text = readText(path10.join(this.root, name, "stat"));
5587
+ const text = readText(path12.join(this.root, name, "stat"));
5199
5588
  const stat = text ? parseProcStat(Number(name), text) : null;
5200
5589
  if (stat) out.set(stat.pid, stat);
5201
5590
  }
5202
5591
  return out;
5203
5592
  }
5204
5593
  stat(pid) {
5205
- const text = readText(path10.join(this.root, String(pid), "stat"));
5594
+ const text = readText(path12.join(this.root, String(pid), "stat"));
5206
5595
  return text ? parseProcStat(pid, text) : null;
5207
5596
  }
5208
5597
  runWaitMs(pid) {
5209
- const text = readText(path10.join(this.root, String(pid), "schedstat"));
5598
+ const text = readText(path12.join(this.root, String(pid), "schedstat"));
5210
5599
  return text ? parseSchedstat(text) : null;
5211
5600
  }
5212
5601
  cmdline(pid) {
5213
- 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();
5214
5603
  }
5215
5604
  /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
5216
5605
  statusKb(pid, key) {
5217
- const text = readText(path10.join(this.root, String(pid), "status"));
5606
+ const text = readText(path12.join(this.root, String(pid), "status"));
5218
5607
  return text ? parseKbField(text, key) : null;
5219
5608
  }
5220
5609
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
5221
5610
  async pssKb(pid) {
5222
5611
  try {
5223
- 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");
5224
5613
  } catch {
5225
5614
  return null;
5226
5615
  }
@@ -5228,29 +5617,29 @@ var ProcFs = class {
5228
5617
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
5229
5618
  resetPeakRss(pid) {
5230
5619
  try {
5231
- fs7.writeFileSync(path10.join(this.root, String(pid), "clear_refs"), "5");
5620
+ fs9.writeFileSync(path12.join(this.root, String(pid), "clear_refs"), "5");
5232
5621
  return true;
5233
5622
  } catch {
5234
5623
  return false;
5235
5624
  }
5236
5625
  }
5237
5626
  cpuTicks() {
5238
- const text = readText(path10.join(this.root, "stat"));
5627
+ const text = readText(path12.join(this.root, "stat"));
5239
5628
  return text ? parseCpuTicks(text) : null;
5240
5629
  }
5241
5630
  /** Total stall time of a resource's pressure file, in µs. */
5242
5631
  pressureUs(resource) {
5243
- const text = readText(path10.join(this.root, "pressure", resource));
5632
+ const text = readText(path12.join(this.root, "pressure", resource));
5244
5633
  return text ? parsePsiTotal(text) : null;
5245
5634
  }
5246
5635
  memAvailableKb() {
5247
- const text = readText(path10.join(this.root, "meminfo"));
5636
+ const text = readText(path12.join(this.root, "meminfo"));
5248
5637
  return text ? parseKbField(text, "MemAvailable") : null;
5249
5638
  }
5250
5639
  /** Open file descriptors of a process. */
5251
5640
  fdCount(pid) {
5252
5641
  try {
5253
- return fs7.readdirSync(path10.join(this.root, String(pid), "fd")).length;
5642
+ return fs9.readdirSync(path12.join(this.root, String(pid), "fd")).length;
5254
5643
  } catch {
5255
5644
  return null;
5256
5645
  }
@@ -5412,10 +5801,10 @@ var PAGE_READ_TIMEOUT_MS = 1e3;
5412
5801
  var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
5413
5802
  var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
5414
5803
  function relativePath(file) {
5415
- 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("/");
5416
5805
  }
5417
5806
  function isLibraryFile(file) {
5418
- 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;
5419
5808
  }
5420
5809
  function userSite(stack) {
5421
5810
  for (const line of (stack ?? "").split("\n").slice(1)) {
@@ -5463,7 +5852,7 @@ function playwrightTestInfoSource() {
5463
5852
  try {
5464
5853
  const packageJson = resolvePlaywrightPackageJson();
5465
5854
  if (!packageJson) return () => null;
5466
- 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"));
5467
5856
  const read = globals.currentTestInfo;
5468
5857
  if (typeof read !== "function") return () => null;
5469
5858
  return () => {
@@ -5492,15 +5881,15 @@ function countHandles() {
5492
5881
  async function settledHandles(start) {
5493
5882
  const end = countHandles();
5494
5883
  if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
5495
- await new Promise((resolve3) => setTimeout(resolve3, 0));
5884
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
5496
5885
  return countHandles();
5497
5886
  }
5498
5887
  function settleWithin(promise, ms) {
5499
5888
  let timer;
5500
5889
  return Promise.race([
5501
5890
  promise.catch(() => void 0),
5502
- new Promise((resolve3) => {
5503
- timer = setTimeout(resolve3, ms);
5891
+ new Promise((resolve5) => {
5892
+ timer = setTimeout(resolve5, ms);
5504
5893
  })
5505
5894
  ]).finally(() => clearTimeout(timer));
5506
5895
  }
@@ -5629,7 +6018,7 @@ var ResourceLedger = class {
5629
6018
  const census = this.census(null);
5630
6019
  if (this.resultsFile) {
5631
6020
  try {
5632
- fs8.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
6021
+ fs10.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
5633
6022
  `);
5634
6023
  } catch {
5635
6024
  }
@@ -6246,6 +6635,13 @@ function createSink() {
6246
6635
  pickOffered: false,
6247
6636
  userPick: null,
6248
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,
6249
6645
  locatorPages: new LocatorPageLog(),
6250
6646
  codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
6251
6647
  serverCoverage: /* @__PURE__ */ new Map(),
@@ -6271,7 +6667,7 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
6271
6667
  async function startCodeReachOnOpenPages() {
6272
6668
  if (!browserCoverageInWorker() || OPEN_PAGES.size === 0) return;
6273
6669
  const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
6274
- 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())]);
6275
6671
  }
6276
6672
  function configuredCodeReachRoots() {
6277
6673
  const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
@@ -6285,7 +6681,7 @@ function configuredCodeReachRoots() {
6285
6681
  }
6286
6682
  } catch {
6287
6683
  }
6288
- return raw.split(path12.delimiter).filter(Boolean);
6684
+ return raw.split(path14.delimiter).filter(Boolean);
6289
6685
  }
6290
6686
  async function stopSinkCodeReach(sink, closing) {
6291
6687
  const stopping = [];
@@ -6334,8 +6730,8 @@ async function drainPendingProbes(capMs) {
6334
6730
  let cap;
6335
6731
  await Promise.race([
6336
6732
  Promise.allSettled(PENDING_PROBES),
6337
- new Promise((resolve3) => {
6338
- cap = setTimeout(resolve3, capMs);
6733
+ new Promise((resolve5) => {
6734
+ cap = setTimeout(resolve5, capMs);
6339
6735
  })
6340
6736
  ]);
6341
6737
  clearTimeout(cap);
@@ -6375,10 +6771,10 @@ async function readWebVitals(page) {
6375
6771
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
6376
6772
  paint[key] = Math.round(entry.startTime);
6377
6773
  }
6378
- const readBuffered = (type, extra) => new Promise((resolve3) => {
6774
+ const readBuffered = (type, extra) => new Promise((resolve5) => {
6379
6775
  try {
6380
6776
  const PO = globalThis.PerformanceObserver;
6381
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve3(null);
6777
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve5(null);
6382
6778
  const out = [];
6383
6779
  const po = new PO((list) => out.push(...list.getEntries()));
6384
6780
  po.observe({ type, buffered: true, ...extra });
@@ -6388,10 +6784,10 @@ async function readWebVitals(page) {
6388
6784
  po.disconnect();
6389
6785
  } catch {
6390
6786
  }
6391
- resolve3(out);
6787
+ resolve5(out);
6392
6788
  }, 0);
6393
6789
  } catch {
6394
- resolve3(null);
6790
+ resolve5(null);
6395
6791
  }
6396
6792
  });
6397
6793
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -6506,12 +6902,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
6506
6902
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
6507
6903
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
6508
6904
  function withTimeout(promise, ms) {
6509
- return new Promise((resolve3, reject) => {
6905
+ return new Promise((resolve5, reject) => {
6510
6906
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
6511
6907
  promise.then(
6512
6908
  (value) => {
6513
6909
  clearTimeout(timer);
6514
- resolve3(value);
6910
+ resolve5(value);
6515
6911
  },
6516
6912
  (error) => {
6517
6913
  clearTimeout(timer);
@@ -6626,6 +7022,7 @@ async function maybeOpenPicker(sink, closing) {
6626
7022
  var INSTRUMENTED_PAGES = /* @__PURE__ */ new WeakSet();
6627
7023
  var OPEN_PAGES = /* @__PURE__ */ new Set();
6628
7024
  var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
7025
+ var OPEN_CONTEXTS = /* @__PURE__ */ new Set();
6629
7026
  var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
6630
7027
  var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
6631
7028
  var ACTION_METHOD_SET = new Set(ACTION_METHODS);
@@ -6746,8 +7143,20 @@ function recordLocatorPage(sink, page, locator, location, arrival) {
6746
7143
  } catch {
6747
7144
  }
6748
7145
  }
6749
- function pausesAt(pause, callerLocation) {
6750
- 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);
6751
7160
  }
6752
7161
  function locatorSource(target, method, args) {
6753
7162
  try {
@@ -6757,41 +7166,219 @@ function locatorSource(target, method, args) {
6757
7166
  }
6758
7167
  return renderFailing({ method, args });
6759
7168
  }
6760
- async function pauseBefore(sink, page, target, callerLocation, action, origin) {
6761
- 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) {
6762
7216
  const testInfo = sink.testInfo;
6763
- if (!pause || !testInfo || !callerLocation) return;
6764
- pause.next = false;
6765
- try {
6766
- const locator = locatorSource(target, origin.method, origin.args);
6767
- const onPick = async (picked) => {
6768
- const pick = {
6769
- 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 },
6770
7227
  ...picked
6771
- };
6772
- applyPickToSnapshots(sink.capturedLocators, pick);
6773
- sink.probedLocations.add(callerLocation);
6774
- const place = callerLocation.slice(0, callerLocation.lastIndexOf(":"));
6775
- console.log(`[piwi] Locator picked at ${place}: ${picked.picked.locator}`);
6776
- await postPickToEditor(
6777
- process.env[PIWI_LOCAL_DEBUG_ENV.editorSend],
6778
- pickPayload(picked.picked.locator, callerLocation)
6779
- );
6780
- };
6781
- 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(
6782
7240
  page,
6783
7241
  () => pauseHere(
6784
7242
  page,
6785
- testInfo,
6786
- { 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
+ },
6787
7249
  { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
6788
- onPick
7250
+ onPick,
7251
+ recorderAt(page, testInfo, location, reason === "page-pause" ? "pause" : "breakpoint"),
7252
+ end.signal
6789
7253
  )
6790
- );
6791
- if (choice === "step") pause.next = true;
6792
- else if (choice === "finish") pause.finished = true;
6793
- } 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();
6794
7380
  }
7381
+ return (options) => own.call(page, options);
6795
7382
  }
6796
7383
  function wrapLocator(page, locator, originMethod, originArgs) {
6797
7384
  return new Proxy(locator, {
@@ -6814,36 +7401,35 @@ function wrapLocator(page, locator, originMethod, originArgs) {
6814
7401
  const isNot = Boolean(callArgs[1]?.isNot);
6815
7402
  const callerLocation = sink ? captureCallerLocation() : null;
6816
7403
  if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
6817
- if (sink?.pause && pausesAt(sink.pause, callerLocation)) {
6818
- 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 = {
6819
7411
  method: originMethod,
6820
- args: originArgs
6821
- });
6822
- }
6823
- if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
6824
- return callMethod(target, prop, callArgs);
6825
- }
6826
- sink.lastActivePage = page;
6827
- const used = {
6828
- method: originMethod,
6829
- args: originArgs,
6830
- raw: `${originMethod}(${JSON.stringify(originArgs)})`
6831
- };
6832
- const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
6833
- let seq = -1;
6834
- if (!alreadyProbed) {
6835
- seq = sink.capturedLocators.length;
6836
- sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
6837
- }
6838
- const result = await callMethod(target, prop, callArgs);
6839
- const matches = result?.matches;
6840
- if (matches === true && !alreadyProbed) {
6841
- if (callerLocation) sink.probedLocations.add(callerLocation);
6842
- startElementCapture(sink, page, target, seq, callerLocation, used);
6843
- } else if (matches === false) {
6844
- 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);
6845
7432
  }
6846
- return result;
6847
7433
  };
6848
7434
  }
6849
7435
  if (!ACTION_METHOD_SET.has(prop)) return original;
@@ -6858,9 +7444,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
6858
7444
  args: originArgs,
6859
7445
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
6860
7446
  };
6861
- if (sink.pause && pausesAt(sink.pause, callerLocation)) {
6862
- await pauseBefore(sink, page, target, callerLocation, String(prop), used);
6863
- }
7447
+ const call = knowLocatorCall(sink, page, target, callerLocation, used);
6864
7448
  const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
6865
7449
  let seq = -1;
6866
7450
  if (!alreadyProbed) {
@@ -6878,8 +7462,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
6878
7462
  } catch (error) {
6879
7463
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
6880
7464
  throw error;
7465
+ } finally {
7466
+ forgetLocatorCall(sink, call);
6881
7467
  }
6882
- if (!alreadyProbed) {
7468
+ if (!alreadyProbed && !call?.picked) {
6883
7469
  if (callerLocation) sink.probedLocations.add(callerLocation);
6884
7470
  startElementCapture(sink, page, target, seq, callerLocation, used);
6885
7471
  }
@@ -6932,12 +7518,17 @@ function instrumentPage(page) {
6932
7518
  }
6933
7519
  const ctx = pageContext(page);
6934
7520
  if (ctx) instrumentContext(ctx);
7521
+ const playwrightPause = playwrightPauseOf(page);
7522
+ if (playwrightPause) {
7523
+ page.pause = () => pauseInPlace(page, playwrightPause, captureCallerLocation());
7524
+ }
6935
7525
  if (typeof page.close === "function") {
6936
7526
  const originalClose = page.close.bind(page);
6937
7527
  page.close = async (...args) => {
6938
7528
  await drainPendingProbes(1e3);
6939
7529
  const sink = currentSink;
6940
7530
  if (sink) {
7531
+ await endShownStops(sink, "closed", { page });
6941
7532
  await stashPageState(sink, { page });
6942
7533
  await maybeOpenPicker(sink, { page });
6943
7534
  await stopSinkCodeReach(sink, { page });
@@ -7026,8 +7617,18 @@ function instrumentPage(page) {
7026
7617
  } catch {
7027
7618
  }
7028
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
+ });
7029
7626
  const onRequestEnd = (request, failed) => {
7030
7627
  const sink = currentSink;
7628
+ if (sink?.releasedRequests.delete(request)) {
7629
+ sink.releasedQuietSince = Date.now();
7630
+ if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
7631
+ }
7031
7632
  if (!sink || RECORDED_OPEN.has(request)) return;
7032
7633
  sink.lastActivePage = page;
7033
7634
  const response = sink.openResponses.get(request) ?? null;
@@ -7133,6 +7734,10 @@ async function awaitFlakeConditions() {
7133
7734
  function instrumentContext(context) {
7134
7735
  if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
7135
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
+ }
7136
7741
  observeOwnApiRequests(context);
7137
7742
  if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
7138
7743
  void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
@@ -7152,6 +7757,7 @@ function instrumentContext(context) {
7152
7757
  await drainPendingProbes(1e3);
7153
7758
  const sink = currentSink;
7154
7759
  if (sink) {
7760
+ await endShownStops(sink, "closed", { context });
7155
7761
  await stashPageState(sink, { context });
7156
7762
  await maybeOpenPicker(sink, { context });
7157
7763
  await stopSinkCodeReach(sink, { context });
@@ -7168,6 +7774,7 @@ function patchBrowser(browser) {
7168
7774
  browser.newPage = async (...args) => {
7169
7775
  const page = await originalNewPage(...args);
7170
7776
  instrumentPage(page);
7777
+ await armBreakpoints([pageContext(page)]);
7171
7778
  await awaitFlakeConditions();
7172
7779
  await maybeStartCodeReach(page);
7173
7780
  return page;
@@ -7176,6 +7783,7 @@ function patchBrowser(browser) {
7176
7783
  browser.newContext = async (...args) => {
7177
7784
  const context = await originalNewContext(...args);
7178
7785
  instrumentContext(context);
7786
+ await armBreakpoints([context]);
7179
7787
  return context;
7180
7788
  };
7181
7789
  if (typeof browser.close === "function") {
@@ -7227,14 +7835,20 @@ async function flushSink(sink, testInfo) {
7227
7835
  let drainDeadline;
7228
7836
  await Promise.race([
7229
7837
  Promise.allSettled(sink.capturePromises),
7230
- new Promise((resolve3) => {
7231
- drainDeadline = setTimeout(resolve3, 2e3);
7838
+ new Promise((resolve5) => {
7839
+ drainDeadline = setTimeout(resolve5, 2e3);
7232
7840
  })
7233
7841
  ]);
7234
7842
  clearTimeout(drainDeadline);
7235
7843
  const page = sink.lastActivePage;
7236
7844
  const pageReadable = page !== null && !isPageClosed(page);
7237
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
+ }
7238
7852
  if (sink.capturedLocators.length > 0) {
7239
7853
  await testInfo.attach(ATTACHMENT_NAMES.locators, {
7240
7854
  contentType: "application/json",
@@ -7435,6 +8049,7 @@ var piwiFixtures = {
7435
8049
  // never double-wraps a page the browser patch already instrumented.
7436
8050
  page: async ({ page }, use) => {
7437
8051
  instrumentPage(page);
8052
+ await armBreakpoints([pageContext(page)]);
7438
8053
  await awaitFlakeConditions();
7439
8054
  await maybeStartCodeReach(page);
7440
8055
  await use(page);
@@ -7455,8 +8070,9 @@ var piwiFixtures = {
7455
8070
  if (pauseSet.size) {
7456
8071
  const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
7457
8072
  if (reason) logPauseSkipOnce(reason);
7458
- else sink.pause = { set: pauseSet, next: false, finished: false };
8073
+ else sink.pause = { set: pauseSet, step: false, finished: false };
7459
8074
  }
8075
+ if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
7460
8076
  if (isFlakeMode()) {
7461
8077
  const plan = loadFlakePlan();
7462
8078
  const role = flakeRoleForTest(plan, {
@@ -7481,9 +8097,12 @@ var piwiFixtures = {
7481
8097
  activeResourceLedger()?.testStarted(testInfo);
7482
8098
  currentSink = sink;
7483
8099
  try {
8100
+ await armBreakpoints(OPEN_CONTEXTS);
7484
8101
  await startCodeReachOnOpenPages();
7485
8102
  await use();
7486
8103
  } finally {
8104
+ await endShownStops(sink, "ended");
8105
+ await awaitReleasedCalls(sink);
7487
8106
  currentSink = null;
7488
8107
  if (sink.probeItem) {
7489
8108
  const applied = sink.probeInterception?.applied() ?? false;