@piwitests/reporter 0.62.0 → 0.63.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.
@@ -41,6 +41,7 @@ __export(capture_fixtures_exports, {
41
41
  codeReachRoots: () => codeReachRoots,
42
42
  computeCoreVitals: () => computeCoreVitals,
43
43
  configuredCodeReachRoots: () => configuredCodeReachRoots,
44
+ errorPlace: () => errorPlace,
44
45
  extendPiwiFixtures: () => extendPiwiFixtures,
45
46
  failureError: () => failureError,
46
47
  intentAt: () => intentAt,
@@ -60,7 +61,7 @@ __export(capture_fixtures_exports, {
60
61
  stopResourceLedger: () => stopResourceLedger
61
62
  });
62
63
  module.exports = __toCommonJS(capture_fixtures_exports);
63
- var path14 = __toESM(require("path"));
64
+ var path15 = __toESM(require("path"));
64
65
  var import_node_zlib2 = require("zlib");
65
66
 
66
67
  // ../picker-dom/src/probe.ts
@@ -519,8 +520,8 @@ function installPickerOverlay(arg) {
519
520
  return root && root !== doc && root.host ? root.host : null;
520
521
  };
521
522
  const targetOf = (e) => {
522
- const path15 = typeof e.composedPath === "function" ? e.composedPath() : null;
523
- const first = path15 && path15.length ? path15[0] : null;
523
+ const path16 = typeof e.composedPath === "function" ? e.composedPath() : null;
524
+ const first = path16 && path16.length ? path16[0] : null;
524
525
  return first && first.nodeType === 1 ? first : e.target;
525
526
  };
526
527
  const buildChain = (raw) => {
@@ -825,14 +826,14 @@ function showAnchorPicker(arg) {
825
826
  } catch {
826
827
  shadowRoots.length = 0;
827
828
  }
828
- const within2 = (node2, scope) => {
829
+ const within3 = (node2, scope) => {
829
830
  if (scope === doc) return true;
830
831
  for (let n = node2; n; n = parentOf(n)) if (n === scope) return true;
831
832
  return false;
832
833
  };
833
834
  const queryAll = (scope, sel) => {
834
835
  const found = Array.from(scope.querySelectorAll(sel));
835
- for (const sr of shadowRoots) if (within2(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
836
+ for (const sr of shadowRoots) if (within3(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
836
837
  return found;
837
838
  };
838
839
  const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
@@ -914,13 +915,17 @@ function showAnchorPicker(arg) {
914
915
  }
915
916
  const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
916
917
  const outline2 = doc.createElement("div");
918
+ outline2.id = "__piwi_anchor_outline";
917
919
  outline2.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #22c55e;background:rgba(34,197,94,.10);border-radius:4px;display:none;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(5,46,22,.5);`;
918
920
  const outlineLabel = doc.createElement("div");
921
+ outlineLabel.id = "__piwi_anchor_label";
919
922
  outlineLabel.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(420px,90vw);background:#0b1120;color:#f9fafb;border:1px solid #22c55e;border-radius:6px;padding:3px 7px;font:12px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
920
923
  const pickedOutline = doc.createElement("div");
924
+ pickedOutline.id = "__piwi_anchor_picked";
921
925
  const pr = el.getBoundingClientRect();
922
926
  pickedOutline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55);left:${pr.left}px;top:${pr.top}px;width:${pr.width}px;height:${pr.height}px;`;
923
927
  const panel = doc.createElement("div");
928
+ panel.id = "__piwi_anchor_panel";
924
929
  panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:min(340px,calc(100vw - 56px));max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
925
930
  const title = doc.createElement("div");
926
931
  title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
@@ -1109,6 +1114,7 @@ function showPickerChoices(arg) {
1109
1114
  }
1110
1115
  const Z = 2147483600;
1111
1116
  const wrap = doc.createElement("div");
1117
+ wrap.id = "__piwi_picker_choices";
1112
1118
  wrap.style.cssText = `position:fixed;inset:0;z-index:${Z + 3};background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;font:13px/1.5 system-ui,sans-serif;`;
1113
1119
  const panel = doc.createElement("div");
1114
1120
  panel.style.cssText = "background:#111827;color:#f9fafb;border-radius:10px;padding:20px;max-width:640px;width:90vw;max-height:70vh;overflow:auto;box-shadow:0 8px 40px rgba(0,0,0,.5);";
@@ -1139,10 +1145,14 @@ function showPickerChoices(arg) {
1139
1145
  }
1140
1146
  panel.appendChild(title);
1141
1147
  panel.appendChild(sub);
1142
- const done = (choice) => {
1143
- g.__piwiPickChoice = choice;
1148
+ const cleanup = () => {
1144
1149
  doc.removeEventListener("keydown", onKey, true);
1145
1150
  wrap.remove();
1151
+ if (g.__piwiChoiceCleanup === cleanup) delete g.__piwiChoiceCleanup;
1152
+ };
1153
+ const done = (choice) => {
1154
+ g.__piwiPickChoice = choice;
1155
+ cleanup();
1146
1156
  };
1147
1157
  const onKey = (e) => {
1148
1158
  if (e.key !== "Escape") return;
@@ -1178,6 +1188,7 @@ function showPickerChoices(arg) {
1178
1188
  });
1179
1189
  panel.appendChild(skip);
1180
1190
  doc.addEventListener("keydown", onKey, true);
1191
+ g.__piwiChoiceCleanup = cleanup;
1181
1192
  wrap.appendChild(panel);
1182
1193
  doc.body.appendChild(wrap);
1183
1194
  }
@@ -1229,7 +1240,10 @@ function showPauseBar(arg) {
1229
1240
  ...arg.record ? [
1230
1241
  ["Record here", "record", "Record steps in this page: the editor writes them before this line", false]
1231
1242
  ] : [],
1232
- ...arg.failure ? [] : [["Finish", "finish", "Run on and pause no more in this test", false]]
1243
+ ...arg.failure ? [] : [["Finish", "finish", "Run on and pause no more in this test", false]],
1244
+ ...arg.end && !arg.failure ? [
1245
+ ["End the test", "end", "Skip the rest of this test: it is reported as skipped", false]
1246
+ ] : []
1233
1247
  ];
1234
1248
  for (const [label, choice, hint, primary] of buttons) {
1235
1249
  const btn = doc.createElement("button");
@@ -1990,21 +2004,21 @@ var MAX_INDETERMINATE = 20;
1990
2004
  var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
1991
2005
  var LEVEL = `${STEP}(?:/${STEP})*`;
1992
2006
  var ELEMENT_PATH_RE = new RegExp(`^${LEVEL}(?:\\|${LEVEL})*$`);
1993
- function isElementPath(path15) {
1994
- return typeof path15 === "string" && path15.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path15);
2007
+ function isElementPath(path16) {
2008
+ return typeof path16 === "string" && path16.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path16);
1995
2009
  }
1996
2010
  var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
1997
2011
  function elementRef(v) {
1998
2012
  if (!v || typeof v !== "object") return null;
1999
- const { path: path15, label } = v;
2000
- if (!isElementPath(path15)) return null;
2001
- return { path: path15, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
2013
+ const { path: path16, label } = v;
2014
+ if (!isElementPath(path16)) return null;
2015
+ return { path: path16, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
2002
2016
  }
2003
2017
  function textPoint(v) {
2004
2018
  if (!v || typeof v !== "object") return null;
2005
- const { path: path15, offset } = v;
2019
+ const { path: path16, offset } = v;
2006
2020
  const at = offsetOf(offset);
2007
- return isElementPath(path15) && at !== null ? { path: path15, offset: at } : null;
2021
+ return isElementPath(path16) && at !== null ? { path: path16, offset: at } : null;
2008
2022
  }
2009
2023
  function sanitizePageInteraction(raw) {
2010
2024
  if (!raw || typeof raw !== "object") return {};
@@ -2036,6 +2050,7 @@ function sanitizePageInteraction(raw) {
2036
2050
 
2037
2051
  // src/internal/capture/locator-healing.ts
2038
2052
  var path = __toESM(require("path"));
2053
+ var import_node_url = require("url");
2039
2054
 
2040
2055
  // ../core/src/locator-fingerprint.ts
2041
2056
  var PRESENT_SIMILARITY = 0.8;
@@ -2286,23 +2301,23 @@ var SUGG_TEXT_ROLES = /* @__PURE__ */ new Set([
2286
2301
  "switch"
2287
2302
  ]);
2288
2303
  var SUGG_FIELD_ROLES = /* @__PURE__ */ new Set(["textbox", "combobox", "searchbox", "spinbutton", "slider"]);
2289
- function failedNameAndRole(failed) {
2290
- if (failed.method === "getByRole") {
2291
- const role = typeof failed.args[0] === "string" ? failed.args[0] : null;
2292
- const opts = failed.args[1];
2304
+ function failedNameAndRole(failed2) {
2305
+ if (failed2.method === "getByRole") {
2306
+ const role = typeof failed2.args[0] === "string" ? failed2.args[0] : null;
2307
+ const opts = failed2.args[1];
2293
2308
  const name = opts && typeof opts.name === "string" ? opts.name : null;
2294
2309
  const level = opts && typeof opts.level === "number" ? opts.level : null;
2295
2310
  return { role, name, level };
2296
2311
  }
2297
- const first = failed.args.find((a) => typeof a === "string");
2312
+ const first = failed2.args.find((a) => typeof a === "string");
2298
2313
  return { role: null, name: typeof first === "string" ? first : null, level: null };
2299
2314
  }
2300
- function renderFailing(failed) {
2301
- const { role, name } = failedNameAndRole(failed);
2302
- if (failed.method === "getByRole") {
2315
+ function renderFailing(failed2) {
2316
+ const { role, name } = failedNameAndRole(failed2);
2317
+ if (failed2.method === "getByRole") {
2303
2318
  return name ? `getByRole(${quoteJsString(role ?? "")}, { name: ${quoteJsString(name)} })` : `getByRole(${quoteJsString(role ?? "")})`;
2304
2319
  }
2305
- return `${failed.method}(${quoteJsString(name ?? "")})`;
2320
+ return `${failed2.method}(${quoteJsString(name ?? "")})`;
2306
2321
  }
2307
2322
  function freshSuggestions(candidate, failedMethod) {
2308
2323
  const out = [];
@@ -2322,9 +2337,9 @@ function freshSuggestions(candidate, failedMethod) {
2322
2337
  else if (SUGG_FIELD_ROLES.has(role)) push(labelLoc);
2323
2338
  return out;
2324
2339
  }
2325
- function suggestLocatorsFromAria(failed, ariaSnapshot) {
2326
- if (!ariaSnapshot || !NAME_BASED_METHODS.has(failed.method)) return null;
2327
- const { role, name, level } = failedNameAndRole(failed);
2340
+ function suggestLocatorsFromAria(failed2, ariaSnapshot) {
2341
+ if (!ariaSnapshot || !NAME_BASED_METHODS.has(failed2.method)) return null;
2342
+ const { role, name, level } = failedNameAndRole(failed2);
2328
2343
  if (!name) return null;
2329
2344
  const candidates = parseAriaCandidates(ariaSnapshot);
2330
2345
  if (candidates.length === 0) return null;
@@ -2332,9 +2347,23 @@ function suggestLocatorsFromAria(failed, ariaSnapshot) {
2332
2347
  if (fingerprintPresent(fingerprint, candidates)) return null;
2333
2348
  const best = matchRenamedElement(fingerprint, candidates)?.candidate;
2334
2349
  if (!best || !best.name) return null;
2335
- const suggestions = freshSuggestions({ role: best.role, name: best.name, level: best.level }, failed.method);
2350
+ const suggestions = freshSuggestions({ role: best.role, name: best.name, level: best.level }, failed2.method);
2336
2351
  if (suggestions.length === 0) return null;
2337
- return { failing: renderFailing(failed), suggestions };
2352
+ return { failing: renderFailing(failed2), suggestions };
2353
+ }
2354
+ function framePath(file, windows = process.platform === "win32") {
2355
+ if (!file.startsWith("file://")) return file;
2356
+ try {
2357
+ return (0, import_node_url.fileURLToPath)(file, { windows });
2358
+ } catch {
2359
+ return null;
2360
+ }
2361
+ }
2362
+ function callSitePath(file, cwd = process.cwd(), windows = process.platform === "win32") {
2363
+ const local = framePath(file, windows);
2364
+ if (local === null) return null;
2365
+ const paths = windows ? path.win32 : path.posix;
2366
+ return paths.relative(cwd, local).split(paths.sep).join("/");
2338
2367
  }
2339
2368
  function captureCallerLocation(stack = new Error().stack ?? "") {
2340
2369
  const lines = stack.split("\n");
@@ -2348,13 +2377,8 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
2348
2377
  prevWasCaptureModule = false;
2349
2378
  continue;
2350
2379
  }
2351
- let file = m[2];
2352
- if (!file || file.startsWith("node:")) {
2353
- prevWasCaptureModule = false;
2354
- continue;
2355
- }
2356
- file = file.replace(/^file:\/\/\/?/, "");
2357
- if (!/\.[a-z]+$/i.test(file)) {
2380
+ const file = m[2] ? framePath(m[2]) : null;
2381
+ if (!file || file.startsWith("node:") || !/\.[a-z]+$/i.test(file)) {
2358
2382
  prevWasCaptureModule = false;
2359
2383
  continue;
2360
2384
  }
@@ -2377,11 +2401,9 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
2377
2401
  }
2378
2402
  let rel = file;
2379
2403
  try {
2380
- rel = path.relative(process.cwd(), file);
2404
+ rel = callSitePath(file) ?? file;
2381
2405
  } catch {
2382
2406
  }
2383
- rel = rel.split(path.sep).join("/");
2384
- if (rel.startsWith("./")) rel = rel.slice(2);
2385
2407
  return `${rel}:${m[3]}:${m[4]}`;
2386
2408
  }
2387
2409
  return null;
@@ -2661,6 +2683,13 @@ var path2 = __toESM(require("path"));
2661
2683
  // src/internal/config/dotenv.ts
2662
2684
  var fs2 = __toESM(require("fs"));
2663
2685
 
2686
+ // src/internal/support/ci.ts
2687
+ function detectCi(env) {
2688
+ return Boolean(
2689
+ env.JENKINS_URL || env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.TRAVIS || env.TF_BUILD || env.BITBUCKET_BUILD_NUMBER || env.TEAMCITY_VERSION || env.CODEBUILD_BUILD_ID || env.bamboo_buildKey || env.CI && env.CI.toLowerCase() !== "false" && env.CI !== "0"
2690
+ );
2691
+ }
2692
+
2664
2693
  // src/internal/config/env.ts
2665
2694
  var DEFAULTS = {
2666
2695
  projectName: "default-project",
@@ -2753,6 +2782,7 @@ var PIWI_ORIGIN_ENV = {
2753
2782
  kind: "PIWI_ORIGIN",
2754
2783
  ref: "PIWI_ORIGIN_REF"
2755
2784
  };
2785
+ var PIWI_SHARD_ENV = "PIWI_SHARD";
2756
2786
  var PIWI_PROBE_ENV = {
2757
2787
  flag: "PIWI_PROBE",
2758
2788
  plan: "PIWI_PROBE_PLAN",
@@ -2817,8 +2847,8 @@ function parseRouteKey(routeKey2) {
2817
2847
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
2818
2848
  }
2819
2849
  function patternToRegex(pattern) {
2820
- const path15 = pattern.split("?")[0] ?? pattern;
2821
- const escaped = path15.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2850
+ const path16 = pattern.split("?")[0] ?? pattern;
2851
+ const escaped = path16.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2822
2852
  return new RegExp(`^${escaped}$`);
2823
2853
  }
2824
2854
  function suitePathEqual(a, b) {
@@ -2841,13 +2871,13 @@ function matchProbeItem(plan, test) {
2841
2871
  function requestMatchesRoute(method, url, routeKey2) {
2842
2872
  const { method: rm, pattern } = parseRouteKey(routeKey2);
2843
2873
  if (rm && rm !== method.toUpperCase()) return false;
2844
- let path15;
2874
+ let path16;
2845
2875
  try {
2846
- path15 = new URL(url).pathname;
2876
+ path16 = new URL(url).pathname;
2847
2877
  } catch {
2848
- path15 = url.split("?")[0] ?? url;
2878
+ path16 = url.split("?")[0] ?? url;
2849
2879
  }
2850
- return patternToRegex(pattern).test(path15);
2880
+ return patternToRegex(pattern).test(path16);
2851
2881
  }
2852
2882
  function createProbeState() {
2853
2883
  return { navigated: false, matches: 0 };
@@ -2869,10 +2899,10 @@ function isProbeMode() {
2869
2899
  function loadPlan() {
2870
2900
  if (planCache !== void 0) return planCache;
2871
2901
  planCache = null;
2872
- const path15 = process.env[PIWI_PROBE_ENV.plan];
2873
- if (path15) {
2902
+ const path16 = process.env[PIWI_PROBE_ENV.plan];
2903
+ if (path16) {
2874
2904
  try {
2875
- planCache = JSON.parse(fs3.readFileSync(path15, "utf8"));
2905
+ planCache = JSON.parse(fs3.readFileSync(path16, "utf8"));
2876
2906
  } catch {
2877
2907
  planCache = null;
2878
2908
  }
@@ -2896,10 +2926,10 @@ function outcomeFromStatus(status, applied) {
2896
2926
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2897
2927
  }
2898
2928
  function recordProbeOutcome(line) {
2899
- const path15 = process.env[PIWI_PROBE_ENV.results];
2900
- if (!path15) return;
2929
+ const path16 = process.env[PIWI_PROBE_ENV.results];
2930
+ if (!path16) return;
2901
2931
  try {
2902
- fs3.appendFileSync(path15, `${JSON.stringify(line)}
2932
+ fs3.appendFileSync(path16, `${JSON.stringify(line)}
2903
2933
  `);
2904
2934
  } catch {
2905
2935
  }
@@ -2954,26 +2984,26 @@ function dropOneField(body) {
2954
2984
  }
2955
2985
  return body;
2956
2986
  }
2957
- function computeFault(fault, input) {
2987
+ function computeFault(fault, input2) {
2958
2988
  const base = {
2959
- status: input.status,
2960
- body: input.body,
2961
- contentType: input.contentType ?? null,
2989
+ status: input2.status,
2990
+ body: input2.body,
2991
+ contentType: input2.contentType ?? null,
2962
2992
  delayMs: 0,
2963
2993
  changed: false
2964
2994
  };
2965
2995
  switch (fault) {
2966
2996
  case "status-500":
2967
- return { ...base, status: 500, changed: input.status !== 500 };
2997
+ return { ...base, status: 500, changed: input2.status !== 500 };
2968
2998
  case "empty-body":
2969
- return { ...base, body: "", changed: input.body.length > 0 };
2999
+ return { ...base, body: "", changed: input2.body.length > 0 };
2970
3000
  case "drop-field": {
2971
- const body = dropOneField(input.body);
2972
- return { ...base, body, changed: body !== input.body };
3001
+ const body = dropOneField(input2.body);
3002
+ return { ...base, body, changed: body !== input2.body };
2973
3003
  }
2974
3004
  case "stale-value": {
2975
- const body = input.priorBody ?? input.body;
2976
- return { ...base, body, changed: body !== input.body };
3005
+ const body = input2.priorBody ?? input2.body;
3006
+ return { ...base, body, changed: body !== input2.body };
2977
3007
  }
2978
3008
  case "slow":
2979
3009
  return { ...base, delayMs: SLOW_FAULT_DELAY_MS, changed: true };
@@ -3467,16 +3497,16 @@ function relativeTestFile(file, base = process.cwd()) {
3467
3497
  return file ? relativeTestPath(file, base) : null;
3468
3498
  }
3469
3499
  function flakeRoleForTest(plan, test) {
3470
- const running = {
3500
+ const running2 = {
3471
3501
  file: relativeTestFile(test.file, test.base),
3472
3502
  title: test.title,
3473
3503
  suite: test.titlePath ? test.titlePath.slice(1, -1) : void 0
3474
3504
  };
3475
- if (flakeTestMatches(plan.test, running) && (plan.test.project === null || plan.test.project === test.project)) {
3505
+ if (flakeTestMatches(plan.test, running2) && (plan.test.project === null || plan.test.project === test.project)) {
3476
3506
  return "target";
3477
3507
  }
3478
3508
  const companion = plan.arm.conditions.some(
3479
- (c) => (c.kind === "alongside" || c.kind === "after") && flakeTestMatches(c.test, running)
3509
+ (c) => (c.kind === "alongside" || c.kind === "after") && flakeTestMatches(c.test, running2)
3480
3510
  );
3481
3511
  return companion ? "companion" : null;
3482
3512
  }
@@ -3577,8 +3607,8 @@ async function installFlakeConditions(page, plan) {
3577
3607
  }
3578
3608
  var FAILED_STATUSES = /* @__PURE__ */ new Set(["failed", "timedOut", "interrupted"]);
3579
3609
  function flakeResultLine(plan, attempt) {
3580
- const failed = FAILED_STATUSES.has(attempt.status);
3581
- const errorSignature = failed && attempt.errorText ? flakeErrorSignature(attempt.errorText) : null;
3610
+ const failed2 = FAILED_STATUSES.has(attempt.status);
3611
+ const errorSignature = failed2 && attempt.errorText ? flakeErrorSignature(attempt.errorText) : null;
3582
3612
  return {
3583
3613
  version: FLAKE_PLAN_VERSION,
3584
3614
  experimentId: plan.experimentId,
@@ -3670,13 +3700,13 @@ function normalizeFunctionRanges(ranges) {
3670
3700
  }
3671
3701
  function normalizeCoveragePath(raw) {
3672
3702
  if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
3673
- const path15 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
3674
- if (!path15 || path15.startsWith("../") || path15.startsWith("/") || /^[A-Za-z]:\//.test(path15)) return null;
3675
- if (/(^|\/)node_modules\//.test(path15)) return null;
3676
- return path15;
3703
+ const path16 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
3704
+ if (!path16 || path16.startsWith("../") || path16.startsWith("/") || /^[A-Za-z]:\//.test(path16)) return null;
3705
+ if (/(^|\/)node_modules\//.test(path16)) return null;
3706
+ return path16;
3677
3707
  }
3678
- function fileEntry(path15, runtime, blob, functions = [], lines = []) {
3679
- const file = { path: path15, runtime };
3708
+ function fileEntry(path16, runtime, blob, functions = [], lines = []) {
3709
+ const file = { path: path16, runtime };
3680
3710
  if (blob && (functions.length > 0 || lines.length > 0)) file.blob = blob;
3681
3711
  if (functions.length > 0) file.functions = functions;
3682
3712
  if (lines.length > 0) file.lines = lines;
@@ -3695,11 +3725,11 @@ function sanitizeCoverage(raw) {
3695
3725
  const files = [];
3696
3726
  for (const item of raw.files) {
3697
3727
  if (!isRecord(item) || !isRuntime(item.runtime)) continue;
3698
- const path15 = normalizeCoveragePath(item.path);
3699
- if (path15 === null) continue;
3728
+ const path16 = normalizeCoveragePath(item.path);
3729
+ if (path16 === null) continue;
3700
3730
  const blob = typeof item.blob === "string" && BLOB_ID.test(item.blob) ? item.blob : void 0;
3701
3731
  files.push(
3702
- fileEntry(path15, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
3732
+ fileEntry(path16, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
3703
3733
  );
3704
3734
  }
3705
3735
  const merged = mergeCoverage(captureOf(files, raw.incomplete === true));
@@ -4241,9 +4271,9 @@ function hideParams(params, count) {
4241
4271
  return isSecretName(decodeSafe(name)) || isSecretValue(decoded) ? `${name}=${hide()}` : part;
4242
4272
  }).join("");
4243
4273
  }
4244
- function hidePath(path15, count) {
4274
+ function hidePath(path16, count) {
4245
4275
  const hide = hider(count);
4246
- return path15.split("/").map((segment) => {
4276
+ return path16.split("/").map((segment) => {
4247
4277
  const [own = "", ...params] = segment.split(";");
4248
4278
  const decoded = decodeSafe(own);
4249
4279
  const kept = decoded !== HIDDEN_VALUE && isSecretValue(decoded) ? hide() : own;
@@ -4253,14 +4283,14 @@ function hidePath(path15, count) {
4253
4283
  function hideSecretsInUrl(url, count = { n: 0 }) {
4254
4284
  const parts = URL_PARTS.exec(url);
4255
4285
  if (!parts) return url;
4256
- const [, scheme = "", authority, path15 = "", query = "", fragment = ""] = parts;
4286
+ const [, scheme = "", authority, path16 = "", query = "", fragment = ""] = parts;
4257
4287
  let out = "";
4258
4288
  if (authority !== void 0) {
4259
4289
  const at = authority.lastIndexOf("@");
4260
4290
  const colon = at >= 0 ? authority.slice(0, at).indexOf(":") : -1;
4261
4291
  out += colon >= 0 && colon < at - 1 ? `${scheme}//${authority.slice(0, colon + 1)}${hider(count)()}${authority.slice(at)}` : `${scheme}//${authority}`;
4262
4292
  }
4263
- out += hidePath(path15, count);
4293
+ out += hidePath(path16, count);
4264
4294
  if (query) out += `?${hideParams(query.slice(1), count)}`;
4265
4295
  if (fragment) {
4266
4296
  const body = fragment.slice(1);
@@ -4291,10 +4321,10 @@ function hideInText(text, count) {
4291
4321
  if (text.length < 4 || !/[A-Za-z0-9]/.test(text)) return text;
4292
4322
  const hide = hider(count);
4293
4323
  let out = hidePem(text, hide);
4294
- out = out.replace(QUERY_PAIR, (all, sep9, name, value) => {
4324
+ out = out.replace(QUERY_PAIR, (all, sep7, name, value) => {
4295
4325
  const decoded = decodeSafe(value);
4296
4326
  if (!value || decoded === HIDDEN_VALUE) return all;
4297
- return isSecretName(name) || isSecretValue(decoded) ? `${sep9}${name}=${hide()}` : all;
4327
+ return isSecretName(name) || isSecretValue(decoded) ? `${sep7}${name}=${hide()}` : all;
4298
4328
  });
4299
4329
  out = out.replace(URL_PASSWORD, (_all, before, _password, at) => `${before}${hide()}${at}`);
4300
4330
  out = out.replace(SET_COOKIE, (_all, before) => `${before}${hide()}`);
@@ -4390,8 +4420,10 @@ async function cleanupPicker(page, ended = false) {
4390
4420
  const g = globalThis;
4391
4421
  if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
4392
4422
  if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
4423
+ if (typeof g.__piwiChoiceCleanup === "function") g.__piwiChoiceCleanup();
4393
4424
  delete g.__piwiPickCleanup;
4394
4425
  delete g.__piwiAnchorCleanup;
4426
+ delete g.__piwiChoiceCleanup;
4395
4427
  delete g.__piwiPickState;
4396
4428
  delete g.__piwiPickChoice;
4397
4429
  delete g.__piwiPickedElement;
@@ -4417,13 +4449,24 @@ function unlessEnded(run, ended) {
4417
4449
  ]);
4418
4450
  }
4419
4451
  async function pickLocator(page, probe, failing, ended) {
4452
+ let polling = false;
4420
4453
  const answered = async (name) => {
4421
4454
  const wait = page.waitForFunction(
4422
- ([key, endedKey]) => globalThis[key] !== void 0 || globalThis[endedKey] === true,
4455
+ ([key, endedKey]) => {
4456
+ const g = globalThis;
4457
+ if (g[endedKey] !== true) return g[key] !== void 0;
4458
+ delete g[endedKey];
4459
+ return true;
4460
+ },
4423
4461
  [name, PICK_ENDED],
4424
4462
  { timeout: 0, polling: 250 }
4425
4463
  );
4426
- await unlessEnded(wait, ended);
4464
+ polling = true;
4465
+ const done = await unlessEnded(
4466
+ wait.then(() => true),
4467
+ ended
4468
+ );
4469
+ if (done) polling = false;
4427
4470
  return !ended?.aborted;
4428
4471
  };
4429
4472
  try {
@@ -4461,25 +4504,25 @@ async function pickLocator(page, probe, failing, ended) {
4461
4504
  chainLeafCount = anchorResult.chainCount;
4462
4505
  }
4463
4506
  }
4464
- const ranked = mergeCandidates(
4507
+ const ranked2 = mergeCandidates(
4465
4508
  generateAlternatives({ ...attrs, accessibleName }),
4466
4509
  generateAnchoredAlternatives({ role, level }, anchors, chainLeafCount)
4467
4510
  );
4468
- if (ranked.length === 0) {
4511
+ if (ranked2.length === 0) {
4469
4512
  console.log("[piwi] No stable locator could be generated for the picked element \u2014 nothing recorded.");
4470
4513
  return null;
4471
4514
  }
4472
4515
  await page.evaluate(showPickerChoices, {
4473
4516
  failing,
4474
- choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
4517
+ choices: ranked2.map((r) => ({ locator: r.locator, score: r.score }))
4475
4518
  });
4476
4519
  if (!await answered("__piwiPickChoice")) return null;
4477
4520
  const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
4478
- if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
4479
- const picked = { ...ranked[choice], pickedByUser: true };
4521
+ if (typeof choice !== "number" || choice < 0 || choice >= ranked2.length) return null;
4522
+ const picked = { ...ranked2[choice], pickedByUser: true };
4480
4523
  return {
4481
4524
  picked,
4482
- alternatives: [picked, ...ranked.filter((_, i) => i !== choice)],
4525
+ alternatives: [picked, ...ranked2.filter((_, i) => i !== choice)],
4483
4526
  element: {
4484
4527
  tagName: attrs.tagName,
4485
4528
  attributes: attrs.attributes,
@@ -4492,27 +4535,27 @@ async function pickLocator(page, probe, failing, ended) {
4492
4535
  ...anchors.length > 0 ? { anchors } : {}
4493
4536
  };
4494
4537
  } finally {
4495
- await cleanupPicker(page, ended?.aborted);
4538
+ await cleanupPicker(page, polling);
4496
4539
  }
4497
4540
  }
4498
- async function runLocatorPicker(page, testInfo, failed, probe) {
4541
+ async function runLocatorPicker(page, testInfo, failed2, probe) {
4499
4542
  try {
4500
4543
  testInfo.setTimeout(0);
4501
- const rendered = failed ? renderFailing(failed) : null;
4544
+ const rendered = failed2 ? renderFailing(failed2) : null;
4502
4545
  console.log(
4503
4546
  `
4504
4547
  [piwi] "${testInfo.title}" ${testInfo.status} \u2014 ${rendered ? "locator picker" : "inspector"} open in the browser: ${rendered ? `click the element that should replace ${rendered}` : "click any element to generate locators for it"} (\u2191/\u2193 to select a parent/child, Esc to skip).`
4505
4548
  );
4506
4549
  const pick = await pickLocator(page, probe, rendered);
4507
4550
  if (!pick) return null;
4508
- const location = failed?.location ?? null;
4551
+ const location = failed2?.location ?? null;
4509
4552
  console.log(
4510
- failed ? `[piwi] Replacement picked for ${rendered}${location ? ` at ${location}` : ""}:
4553
+ failed2 ? `[piwi] Replacement picked for ${rendered}${location ? ` at ${location}` : ""}:
4511
4554
  [piwi] ${pick.picked.locator}` : `[piwi] Locator picked while inspecting:
4512
4555
  [piwi] ${pick.picked.locator}`
4513
4556
  );
4514
4557
  return {
4515
- failing: failed ? { method: failed.method, args: failed.args, rendered, location } : null,
4558
+ failing: failed2 ? { method: failed2.method, args: failed2.args, rendered, location } : null,
4516
4559
  ...pick
4517
4560
  };
4518
4561
  } catch {
@@ -4800,8 +4843,39 @@ function askAgain(state, asked) {
4800
4843
  function goOn(state, answer) {
4801
4844
  if (answer === "step") state.step = true;
4802
4845
  else if (answer === "finish") state.finished = true;
4846
+ else if (answer === "end") {
4847
+ state.finished = true;
4848
+ stopShown(state);
4849
+ }
4803
4850
  return wantsStops(state) ? "next" : "resume";
4804
4851
  }
4852
+ var ENDED_REASON = "Ended from Piwi";
4853
+ var OPTIONS_AT = {
4854
+ fill: 1,
4855
+ selectOption: 1,
4856
+ press: 1,
4857
+ type: 1,
4858
+ pressSequentially: 1,
4859
+ setInputFiles: 1,
4860
+ dragTo: 1,
4861
+ dispatchEvent: 2,
4862
+ _expect: 1
4863
+ };
4864
+ function withEndSignal(method, args, signal) {
4865
+ const at = OPTIONS_AT[method] ?? 0;
4866
+ const given = args[at];
4867
+ if (given !== void 0 && (typeof given !== "object" || given === null || Array.isArray(given))) return null;
4868
+ const options = given ?? {};
4869
+ let combined = signal;
4870
+ if (options.signal !== void 0) {
4871
+ if (!(options.signal instanceof AbortSignal) || typeof AbortSignal.any !== "function") return null;
4872
+ combined = AbortSignal.any([options.signal, signal]);
4873
+ }
4874
+ const next = [...args];
4875
+ while (next.length < at) next.push(void 0);
4876
+ next[at] = { ...options, signal: combined };
4877
+ return next;
4878
+ }
4805
4879
  function contextDebugger(context) {
4806
4880
  const dbg = context.debugger;
4807
4881
  return typeof dbg?.requestPause === "function" && typeof dbg.on === "function" ? dbg : null;
@@ -4901,12 +4975,12 @@ function pickPayload(locator, callerLocation) {
4901
4975
  return at ? { kind: "locator", text: locator, at } : { kind: "locator", text: locator };
4902
4976
  }
4903
4977
  var postFailureLogged = false;
4904
- async function postPickToEditor(pairingAddress, payload, fetchImpl = fetch) {
4978
+ async function postPickToEditor(pairingAddress, payload, fetchImpl = fetch, host2 = "the editor") {
4905
4979
  if (!pairingAddress) return false;
4906
4980
  const logOnce = (why) => {
4907
4981
  if (postFailureLogged) return;
4908
4982
  postFailureLogged = true;
4909
- console.log(`[piwi] Could not send the picked locator to the editor: ${why}.`);
4983
+ console.log(`[piwi] Could not send the picked locator to ${host2}: ${why}.`);
4910
4984
  };
4911
4985
  const pairing = parsePairing(pairingAddress);
4912
4986
  if (!pairing) {
@@ -4921,7 +4995,7 @@ async function postPickToEditor(pairingAddress, payload, fetchImpl = fetch) {
4921
4995
  signal: AbortSignal.timeout(5e3)
4922
4996
  });
4923
4997
  if (response.ok) return true;
4924
- logOnce(`the editor answered ${response.status}`);
4998
+ logOnce(`${host2} answered ${response.status}`);
4925
4999
  } catch (error) {
4926
5000
  logOnce(error instanceof Error ? error.message : String(error));
4927
5001
  }
@@ -4979,6 +5053,33 @@ async function within(run, ms) {
4979
5053
  clearTimeout(timer);
4980
5054
  }
4981
5055
  }
5056
+ function eitherSignal(a, b) {
5057
+ const either = new AbortController();
5058
+ const abort = () => either.abort();
5059
+ if (a.aborted || b.aborted) abort();
5060
+ a.addEventListener("abort", abort, { once: true });
5061
+ b.addEventListener("abort", abort, { once: true });
5062
+ return either.signal;
5063
+ }
5064
+ async function viewHere(page, probe, onPick, onRecord, ended, intents, onIntent, viewEnded2) {
5065
+ for (; ; ) {
5066
+ const intent = await unlessEnded(intents.wait(), ended);
5067
+ intents.answered();
5068
+ if (!intent || ended.aborted) return;
5069
+ const signal = eitherSignal(ended, viewEnded2());
5070
+ try {
5071
+ if (intent === "pick") {
5072
+ const picked = await pickLocator(page, probe, null, signal).catch(() => null);
5073
+ if (picked && !signal.aborted) await onPick(picked);
5074
+ } else if (intent === "record") {
5075
+ await onRecord?.(signal);
5076
+ } else {
5077
+ await onIntent?.(intent, signal);
5078
+ }
5079
+ } catch {
5080
+ }
5081
+ }
5082
+ }
4982
5083
  async function pauseHere(page, point, probe, onPick, onRecord, ended, intents, onIntent) {
4983
5084
  let shown2 = null;
4984
5085
  try {
@@ -4993,10 +5094,11 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended, intents, o
4993
5094
  action: point.call,
4994
5095
  locator: null,
4995
5096
  record: !!onRecord,
4996
- ...point.failure ? { failure: true } : {}
5097
+ ...point.failure ? { failure: true } : {},
5098
+ ...point.end ? { end: true } : {}
4997
5099
  };
4998
5100
  console.log(
4999
- point.failure ? `[piwi] The test failed at ${callSiteLine(point.location)}: Resume, Pick a locator${onRecord ? " or Record here" : ""} in the browser (Esc resumes, and the test ends as it failed).` : `[piwi] Paused at ${callSiteLine(point.location)} before ${point.call}: Resume, Step, Pick a locator${onRecord ? ", Record here" : ""} or Finish in the browser (Esc resumes).`
5101
+ point.failure ? `[piwi] The test failed at ${callSiteLine(point.location)}: Resume, Pick a locator${onRecord ? " or Record here" : ""} in the browser (Esc resumes, and the test ends as it failed).` : `[piwi] Paused at ${callSiteLine(point.location)} before ${point.call}: Resume, Step, Pick a locator${onRecord ? ", Record here" : ""}, Finish${point.end ? " or End the test" : ""} in the browser (Esc resumes).`
5000
5102
  );
5001
5103
  let navigated = 0;
5002
5104
  for (; ; ) {
@@ -5014,7 +5116,7 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended, intents, o
5014
5116
  if (raised) await unlessEnded(raised, ended);
5015
5117
  if (ended?.aborted) return "resume";
5016
5118
  navigated = 0;
5017
- if (choice === "resume" || choice === "step" || choice === "finish") return choice;
5119
+ if (choice === "resume" || choice === "step" || choice === "finish" || choice === "end") return choice;
5018
5120
  if (choice === "record" && onRecord) {
5019
5121
  await unlessEnded(
5020
5122
  onRecord(ended).catch(() => void 0),
@@ -5099,52 +5201,504 @@ function locatorOn(page, source) {
5099
5201
  }
5100
5202
  }
5101
5203
 
5102
- // src/internal/capture/region-snapshot.ts
5103
- function regionSnapshotWithoutSecrets(snapshot, secrets) {
5104
- const hidden = new Set(secrets.filter(Boolean));
5105
- const kept = [];
5204
+ // src/internal/capture/pick-at.ts
5205
+ var LINE = /^(\s*)- ([a-z][\w-]*)(?: "(?:[^"\\]|\\.)*")?((?: \[[^\]\n]*\])*)(?::.*)?$/;
5206
+ var REF = /\[ref=(e\d+)\]/;
5207
+ var NUMBER = "(-?\\d+(?:\\.\\d+)?)";
5208
+ var BOX = new RegExp(`\\[box=${NUMBER},${NUMBER},${NUMBER},${NUMBER}\\]`);
5209
+ function snapshotBoxes(snapshot) {
5210
+ const boxes = [];
5211
+ const open = [];
5212
+ let unit = 0;
5106
5213
  for (const line of snapshot.split("\n")) {
5107
- const box = /^(\s*- textbox(?: "(?:[^"\\]|\\.)*")?(?: \[[^\]]*\])*): (.*)$/.exec(line);
5108
- if (box && hidden.size && hidden.has(yamlScalar(box[2]))) {
5109
- kept.push({ line: box[1], lostChild: false });
5110
- continue;
5214
+ const m = LINE.exec(line);
5215
+ if (!m) continue;
5216
+ const indent = m[1].length;
5217
+ if (indent > 0 && unit === 0) unit = indent;
5218
+ while (open.length && open[open.length - 1].indent >= indent) open.pop();
5219
+ const attrs = m[3] ?? "";
5220
+ const ref = REF.exec(attrs)?.[1];
5221
+ const box = BOX.exec(attrs);
5222
+ if (!ref || !box) continue;
5223
+ const [x, y, width, height] = box.slice(1).map(Number);
5224
+ if (!(width > 0) || !(height > 0)) continue;
5225
+ const depth = unit ? Math.round(indent / unit) : 0;
5226
+ boxes.push({ ref, role: m[2], x, y, width, height, depth, ancestors: open.map((o) => o.ref) });
5227
+ open.push({ indent, ref });
5228
+ }
5229
+ return boxes;
5230
+ }
5231
+ function boxesAt(boxes, x, y) {
5232
+ const under = boxes.filter((box) => x >= box.x && x < box.x + box.width && y >= box.y && y < box.y + box.height);
5233
+ const holding = new Set(under.flatMap((box) => box.ancestors));
5234
+ const area = (box) => box.width * box.height;
5235
+ return under.sort(
5236
+ (a, b) => Number(holding.has(a.ref)) - Number(holding.has(b.ref)) || area(a) - area(b) || b.depth - a.depth
5237
+ );
5238
+ }
5239
+ var PIWI_OVERLAY = '[id^="__piwi"], [id^="piwi-"][id$="-host"]';
5240
+ var MAX_UNDER = 24;
5241
+ var MAX_CHECKED = 8;
5242
+ var NORMALIZED_SCORE = 60;
5243
+ var PICK_AT_NEEDS = "Pick on the image needs Playwright 1.64 or later (page.getByRef and the boxes of page.ariaSnapshot): update @playwright/test.";
5244
+ var NO_ELEMENT = "No element there: pick an element of the page, not its background.";
5245
+ var IN_IFRAME = "That point is in an iframe, whose elements Pick on the image does not reach: use Pick in the test's window.";
5246
+ function canPickAt(page) {
5247
+ return typeof page.getByRef === "function";
5248
+ }
5249
+ var hitIn = ({ x, y, overlay, elements }) => {
5250
+ let scope = globalThis.document;
5251
+ let hit = null;
5252
+ for (let depth = 0; scope && depth < 32; depth++) {
5253
+ const within3 = scope;
5254
+ const top = within3.elementsFromPoint(x, y).find((el) => el.getRootNode?.() === within3 && !el.closest?.(overlay));
5255
+ if (!top) break;
5256
+ hit = top;
5257
+ scope = top.shadowRoot ?? null;
5258
+ }
5259
+ if (!hit) return null;
5260
+ for (let node = hit; node; ) {
5261
+ const at = elements.indexOf(node);
5262
+ if (at >= 0) return at;
5263
+ node = node.assignedSlot ?? node.parentElement ?? node.parentNode?.host ?? null;
5264
+ }
5265
+ return -1;
5266
+ };
5267
+ async function pickAtPoint(page, probe, x, y) {
5268
+ if (!canPickAt(page)) return { refusal: PICK_AT_NEEDS };
5269
+ const snapshot = await page.ariaSnapshot({ mode: "ai", boxes: true, timeout: 5e3 });
5270
+ if (!/\[box=/.test(snapshot)) return { refusal: PICK_AT_NEEDS };
5271
+ const under = boxesAt(snapshotBoxes(snapshot), x, y).slice(0, MAX_UNDER);
5272
+ const byRef2 = (ref) => page.getByRef(ref);
5273
+ const handles = await Promise.all(
5274
+ under.map(
5275
+ (box) => byRef2(box.ref).elementHandle({ timeout: 2e3 }).catch(() => null)
5276
+ )
5277
+ );
5278
+ try {
5279
+ const hit = await page.evaluate(hitIn, { x, y, overlay: PIWI_OVERLAY, elements: handles });
5280
+ const tested = hit !== null && hit >= 0;
5281
+ for (const [i, box] of under.entries()) {
5282
+ if (tested && i !== hit) continue;
5283
+ if (box.role === "iframe") return { refusal: IN_IFRAME };
5284
+ if (box.depth === 0 && box.role === "generic") return { refusal: NO_ELEMENT };
5285
+ const handle = handles[i];
5286
+ if (!handle) continue;
5287
+ const piwi = !tested && await handle.evaluate(
5288
+ // A browser-side element, hence the cast: this package has no DOM types.
5289
+ (el, overlay) => !!el.closest(overlay),
5290
+ PIWI_OVERLAY
5291
+ );
5292
+ if (!piwi) return await rank(page, probe, byRef2(box.ref), handle);
5111
5293
  }
5112
- const url = /^\s*- \/url: (.*)$/.exec(line);
5113
- if (url && hideSecretsInUrl(yamlScalar(url[1])) !== yamlScalar(url[1])) {
5114
- let parent = kept.length - 1;
5115
- while (parent >= 0 && indentOf(kept[parent].line) >= indentOf(line)) parent--;
5116
- if (parent >= 0) kept[parent].lostChild = true;
5117
- continue;
5294
+ return { refusal: NO_ELEMENT };
5295
+ } finally {
5296
+ await Promise.all(handles.map((handle) => handle?.dispose().catch(() => void 0)));
5297
+ }
5298
+ }
5299
+ async function rankedCandidates(probe, locator, handle, testIdAttribute = null) {
5300
+ const attrs = await handle.evaluate(probe.fn, probe.arg);
5301
+ const accessibleName = approximateAccessibleName({ ...attrs, accessibleName: null });
5302
+ const candidates = generateAlternatives({ ...attrs, accessibleName }, { testIdAttribute });
5303
+ const normalize2 = locator.normalize;
5304
+ const normalized = typeof normalize2 === "function" ? await normalize2.call(locator).then(String, () => null) : null;
5305
+ if (normalized && !candidates.some((c) => c.locator === normalized)) {
5306
+ const leaf = parseLeafLocatorCall(normalized);
5307
+ candidates.push({ locator: normalized, method: leaf?.method ?? "locator", args: {}, score: NORMALIZED_SCORE });
5308
+ candidates.sort((a, b) => b.score - a.score);
5309
+ }
5310
+ return { attrs, accessibleName, candidates };
5311
+ }
5312
+ async function findsAlone(page, candidate, handle) {
5313
+ const built = locatorOn(page, candidate);
5314
+ if (!built || await built.count().catch(() => 0) !== 1) return false;
5315
+ return built.evaluate((el, target) => el === target, handle, { timeout: 2e3 }).catch(() => false);
5316
+ }
5317
+ async function rank(page, probe, locator, handle) {
5318
+ const { attrs, accessibleName, candidates } = await rankedCandidates(probe, locator, handle);
5319
+ let found = null;
5320
+ for (const candidate of candidates.slice(0, MAX_CHECKED)) {
5321
+ if (await findsAlone(page, candidate.locator, handle)) {
5322
+ found = candidate;
5323
+ break;
5118
5324
  }
5119
- kept.push({ line, lostChild: false });
5120
5325
  }
5121
- return kept.map(({ line, lostChild }, i) => {
5122
- const next = kept[i + 1];
5123
- const childless = !next || indentOf(next.line) <= indentOf(line);
5124
- return lostChild && childless && line.endsWith(":") ? line.slice(0, -1) : line;
5125
- }).join("\n");
5326
+ if (!found) return { refusal: "Piwi found no locator that finds that element alone on the page." };
5327
+ const picked = { ...found, pickedByUser: true };
5328
+ const element = {
5329
+ tagName: attrs.tagName,
5330
+ attributes: attrs.attributes,
5331
+ textContent: attrs.textContent,
5332
+ accessibleName,
5333
+ center: attrs.center,
5334
+ ...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
5335
+ ...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
5336
+ };
5337
+ return { picked: { picked, alternatives: [picked, ...candidates.filter((c) => c !== found)], element } };
5126
5338
  }
5127
- function indentOf(line) {
5128
- return line.length - line.trimStart().length;
5339
+
5340
+ // ../core/src/run-view.ts
5341
+ var MAX_VIEW_WIDTH = 1280;
5342
+ var MAX_VIEW_HEIGHT = 800;
5343
+ var VIEW_QUALITY = 60;
5344
+ var MAX_VIEW_FRAME = 384 * 1024;
5345
+ var MAX_VIEW_FRAME_BASE64 = 4 * Math.ceil(MAX_VIEW_FRAME / 3);
5346
+ var MAX_VIEW_FPS = 10;
5347
+ var MAX_VIEW_EVENTS = 50;
5348
+ var MAX_VIEW_EVENT_RATE = 200;
5349
+ var MAX_VIEW_TEXT = 1e3;
5350
+ var MAX_VIEW_COORD = 1e4;
5351
+ var MAX_VIEW_WHEEL = 1e4;
5352
+ var MAX_COMBO_KEYS = 5;
5353
+ var KEY_NAMES = /* @__PURE__ */ new Set([
5354
+ "Escape",
5355
+ "F1",
5356
+ "F2",
5357
+ "F3",
5358
+ "F4",
5359
+ "F5",
5360
+ "F6",
5361
+ "F7",
5362
+ "F8",
5363
+ "F9",
5364
+ "F10",
5365
+ "F11",
5366
+ "F12",
5367
+ "Backquote",
5368
+ "Digit1",
5369
+ "Digit2",
5370
+ "Digit3",
5371
+ "Digit4",
5372
+ "Digit5",
5373
+ "Digit6",
5374
+ "Digit7",
5375
+ "Digit8",
5376
+ "Digit9",
5377
+ "Digit0",
5378
+ "Minus",
5379
+ "Equal",
5380
+ "Backslash",
5381
+ "Backspace",
5382
+ "Tab",
5383
+ "KeyQ",
5384
+ "KeyW",
5385
+ "KeyE",
5386
+ "KeyR",
5387
+ "KeyT",
5388
+ "KeyY",
5389
+ "KeyU",
5390
+ "KeyI",
5391
+ "KeyO",
5392
+ "KeyP",
5393
+ "BracketLeft",
5394
+ "BracketRight",
5395
+ "CapsLock",
5396
+ "KeyA",
5397
+ "KeyS",
5398
+ "KeyD",
5399
+ "KeyF",
5400
+ "KeyG",
5401
+ "KeyH",
5402
+ "KeyJ",
5403
+ "KeyK",
5404
+ "KeyL",
5405
+ "Semicolon",
5406
+ "Quote",
5407
+ "Enter",
5408
+ "ShiftLeft",
5409
+ "Shift",
5410
+ "KeyZ",
5411
+ "KeyX",
5412
+ "KeyC",
5413
+ "KeyV",
5414
+ "KeyB",
5415
+ "KeyN",
5416
+ "KeyM",
5417
+ "Comma",
5418
+ "Period",
5419
+ "Slash",
5420
+ "ShiftRight",
5421
+ "ControlLeft",
5422
+ "Control",
5423
+ "MetaLeft",
5424
+ "Meta",
5425
+ "AltLeft",
5426
+ "Alt",
5427
+ "Space",
5428
+ "AltRight",
5429
+ "AltGraph",
5430
+ "MetaRight",
5431
+ "ContextMenu",
5432
+ "ControlRight",
5433
+ "PrintScreen",
5434
+ "ScrollLock",
5435
+ "Pause",
5436
+ "PageUp",
5437
+ "PageDown",
5438
+ "Insert",
5439
+ "Delete",
5440
+ "Home",
5441
+ "End",
5442
+ "ArrowLeft",
5443
+ "ArrowUp",
5444
+ "ArrowRight",
5445
+ "ArrowDown",
5446
+ "AudioVolumeMute",
5447
+ "AudioVolumeDown",
5448
+ "AudioVolumeUp",
5449
+ "MediaTrackNext",
5450
+ "MediaTrackPrevious",
5451
+ "MediaPlayPause",
5452
+ "NumLock",
5453
+ "NumpadDivide",
5454
+ "NumpadMultiply",
5455
+ "NumpadSubtract",
5456
+ "Numpad7",
5457
+ "Numpad8",
5458
+ "Numpad9",
5459
+ "Numpad4",
5460
+ "Numpad5",
5461
+ "Numpad6",
5462
+ "NumpadAdd",
5463
+ "Numpad1",
5464
+ "Numpad2",
5465
+ "Numpad3",
5466
+ "Numpad0",
5467
+ "NumpadDecimal",
5468
+ "NumpadEnter",
5469
+ "ControlOrMeta"
5470
+ ]);
5471
+ var MODIFIERS = /* @__PURE__ */ new Set(["Alt", "Control", "Meta", "Shift", "ControlOrMeta"]);
5472
+ function isViewTextControl(char) {
5473
+ const code = char.charCodeAt(0);
5474
+ return code < 32 && code !== 9 && code !== 10 || code === 127;
5475
+ }
5476
+ function isViewKey(key) {
5477
+ if (typeof key !== "string") return false;
5478
+ if (key.length === 1) return key >= " " && key <= "~";
5479
+ return KEY_NAMES.has(key);
5480
+ }
5481
+ function isViewKeyCombo(combo) {
5482
+ if (typeof combo !== "string" || !combo || combo.length > 64) return false;
5483
+ const keys = [];
5484
+ let building = "";
5485
+ for (const char of combo) {
5486
+ if (char === "+" && building) {
5487
+ keys.push(building);
5488
+ building = "";
5489
+ } else {
5490
+ building += char;
5491
+ }
5492
+ }
5493
+ keys.push(building);
5494
+ if (keys.length > MAX_COMBO_KEYS) return false;
5495
+ const key = keys.pop();
5496
+ return isViewKey(key) && keys.every((modifier) => MODIFIERS.has(modifier));
5129
5497
  }
5130
- function yamlScalar(text) {
5131
- const value = text.trim();
5132
- if (value.startsWith('"')) {
5133
- try {
5134
- return String(JSON.parse(value));
5135
- } catch {
5136
- return value;
5498
+ function isObject2(value) {
5499
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5500
+ }
5501
+ function isNumber(value, min, max) {
5502
+ return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
5503
+ }
5504
+ function isViewPoint(x, y) {
5505
+ return isNumber(x, 0, MAX_VIEW_COORD) && isNumber(y, 0, MAX_VIEW_COORD);
5506
+ }
5507
+ var BUTTONS = ["left", "right", "middle"];
5508
+ function readEvent(value) {
5509
+ if (!isObject2(value)) return "is not an event.";
5510
+ const { type, action } = value;
5511
+ if (type === "mouse" || type === "wheel") {
5512
+ const { x, y } = value;
5513
+ if (!isViewPoint(x, y)) return `needs x and y, CSS pixels from 0 to ${MAX_VIEW_COORD}.`;
5514
+ const at = { x, y };
5515
+ if (type === "wheel") {
5516
+ const { deltaX, deltaY } = value;
5517
+ if (!isNumber(deltaX, -MAX_VIEW_WHEEL, MAX_VIEW_WHEEL) || !isNumber(deltaY, -MAX_VIEW_WHEEL, MAX_VIEW_WHEEL)) {
5518
+ return `needs deltaX and deltaY, pixels from -${MAX_VIEW_WHEEL} to ${MAX_VIEW_WHEEL}.`;
5519
+ }
5520
+ return { type, ...at, deltaX, deltaY };
5521
+ }
5522
+ if (action === "move") return { type, action, ...at };
5523
+ if (action !== "down" && action !== "up" && action !== "click") {
5524
+ return "is a mouse event whose action is not move, down, up or click.";
5525
+ }
5526
+ const button = value.button === void 0 ? "left" : value.button;
5527
+ if (typeof button !== "string" || !BUTTONS.includes(button))
5528
+ return "names a button other than left, right or middle.";
5529
+ const clickCount = value.clickCount === void 0 && action !== "click" ? void 0 : value.clickCount ?? 1;
5530
+ if (clickCount !== void 0 && (!isNumber(clickCount, 1, 3) || !Number.isInteger(clickCount))) {
5531
+ return "needs a clickCount from 1 to 3.";
5532
+ }
5533
+ if (action !== "click") {
5534
+ return { type, action, ...at, button, ...clickCount === void 0 ? {} : { clickCount } };
5137
5535
  }
5536
+ return { type, action, ...at, button, clickCount };
5138
5537
  }
5139
- if (value.startsWith("'") && value.endsWith("'") && value.length > 1) return value.slice(1, -1).replace(/''/g, "'");
5140
- return value;
5538
+ if (type === "key") {
5539
+ if (action !== "down" && action !== "up" && action !== "press") {
5540
+ return "is a key event whose action is not down, up or press.";
5541
+ }
5542
+ const { key } = value;
5543
+ const known = action === "press" ? isViewKeyCombo(key) : isViewKey(key);
5544
+ return known ? { type, action, key } : `names a key Playwright does not play: ${JSON.stringify(String(key).slice(0, 64))}.`;
5545
+ }
5546
+ if (type === "text") {
5547
+ const { text } = value;
5548
+ if (typeof text !== "string" || !text || text.length > MAX_VIEW_TEXT || [...text].some(isViewTextControl)) {
5549
+ return `needs a text of 1 to ${MAX_VIEW_TEXT} characters, with no control character but a tab or a line break.`;
5550
+ }
5551
+ return { type, text };
5552
+ }
5553
+ return "is of a type other than mouse, wheel, key or text.";
5554
+ }
5555
+ function readViewInput(value) {
5556
+ if (!Array.isArray(value) || value.length === 0) return "The input needs its events.";
5557
+ if (value.length > MAX_VIEW_EVENTS) return `The input carries ${MAX_VIEW_EVENTS} events at most.`;
5558
+ const events = [];
5559
+ for (const [i, raw] of value.entries()) {
5560
+ const event = readEvent(raw);
5561
+ if (typeof event === "string") return `Event ${i + 1} ${event}`;
5562
+ events.push(event);
5563
+ }
5564
+ return events;
5141
5565
  }
5566
+ function isViewSize(width, height) {
5567
+ return isNumber(width, 1, MAX_VIEW_COORD) && Number.isInteger(width) && isNumber(height, 1, MAX_VIEW_COORD) && Number.isInteger(height);
5568
+ }
5569
+ function viewSize(viewport) {
5570
+ return fitViewSize(viewport, MAX_VIEW_WIDTH, MAX_VIEW_HEIGHT);
5571
+ }
5572
+ function fitViewSize(viewport, maxWidth, maxHeight) {
5573
+ const width = Math.max(2, Math.round(viewport.width));
5574
+ const height = Math.max(2, Math.round(viewport.height));
5575
+ const scale = Math.min(1, maxWidth / width, maxHeight / height);
5576
+ return { width: Math.max(2, Math.floor(width * scale)), height: Math.max(2, Math.floor(height * scale)) };
5577
+ }
5578
+ var RateWindow = class {
5579
+ constructor(limit, now = Date.now) {
5580
+ this.limit = limit;
5581
+ this.now = now;
5582
+ this.start = -Infinity;
5583
+ this.used = 0;
5584
+ }
5585
+ take(n) {
5586
+ const at = this.now();
5587
+ if (at - this.start >= 1e3) {
5588
+ this.start = at;
5589
+ this.used = 0;
5590
+ }
5591
+ if (this.used + n > this.limit) return false;
5592
+ this.used += n;
5593
+ return true;
5594
+ }
5595
+ };
5596
+ var LatestPacer = class {
5597
+ constructor(options) {
5598
+ this.options = options;
5599
+ this.pending = null;
5600
+ this.timer = null;
5601
+ this.last = -Infinity;
5602
+ this.closed = false;
5603
+ this.gap = options.intervalMs;
5604
+ }
5605
+ offer(item) {
5606
+ if (this.closed) return;
5607
+ this.pending = { item };
5608
+ this.pump();
5609
+ }
5610
+ /** Whether an item waits. */
5611
+ get waiting() {
5612
+ return this.pending !== null;
5613
+ }
5614
+ /** Send the item waiting when its time came and the way out is free; else try again later. */
5615
+ pump() {
5616
+ if (this.closed || !this.pending || this.timer) return;
5617
+ const now = this.options.now?.() ?? Date.now();
5618
+ const wait = this.last + this.gap - now;
5619
+ const busy = wait <= 0 && !!this.options.busy?.();
5620
+ if (wait > 0 || busy) {
5621
+ this.timer = setTimeout(
5622
+ () => {
5623
+ this.timer = null;
5624
+ this.pump();
5625
+ },
5626
+ busy ? this.options.intervalMs : wait
5627
+ );
5628
+ this.timer.unref?.();
5629
+ return;
5630
+ }
5631
+ const { item } = this.pending;
5632
+ this.pending = null;
5633
+ this.last = now;
5634
+ this.gap = Math.max(this.options.intervalMs, this.options.spacing?.(item) ?? 0);
5635
+ this.options.send(item);
5636
+ }
5637
+ /** Nothing more is sent: the item waiting is dropped. */
5638
+ close() {
5639
+ this.closed = true;
5640
+ this.pending = null;
5641
+ if (this.timer) clearTimeout(this.timer);
5642
+ this.timer = null;
5643
+ }
5644
+ };
5645
+ var FrameAcks = class {
5646
+ constructor(intervalMs, now = Date.now) {
5647
+ this.intervalMs = intervalMs;
5648
+ this.now = now;
5649
+ this.due = -Infinity;
5650
+ this.waiting = null;
5651
+ this.closed = false;
5652
+ }
5653
+ next() {
5654
+ if (this.closed || this.waiting) return void 0;
5655
+ const now = this.now();
5656
+ this.due = Math.min(Math.max(now, this.due + this.intervalMs), now + this.intervalMs);
5657
+ if (this.due <= now) return void 0;
5658
+ return new Promise((resolve5) => {
5659
+ const timer = setTimeout(() => this.release(), this.due - now);
5660
+ timer.unref?.();
5661
+ this.waiting = { timer, resolve: resolve5 };
5662
+ });
5663
+ }
5664
+ /** Whether a frame waits for its acknowledgment. */
5665
+ get holding() {
5666
+ return this.waiting !== null;
5667
+ }
5668
+ /** The frame waiting is acknowledged now, and every later one at once. */
5669
+ close() {
5670
+ this.closed = true;
5671
+ this.release();
5672
+ }
5673
+ release() {
5674
+ const waiting = this.waiting;
5675
+ this.waiting = null;
5676
+ if (!waiting) return;
5677
+ clearTimeout(waiting.timer);
5678
+ waiting.resolve();
5679
+ }
5680
+ };
5142
5681
 
5143
5682
  // ../core/src/run-channel.ts
5144
5683
  var RUN_CHANNEL_ENV = "PIWI_RUN_CHANNEL";
5684
+ var RUN_REF_ENV = "PIWI_RUN_REF";
5145
5685
  var RUN_HOLDS = ["failure"];
5146
5686
  var RUN_INTENTS = ["pick", "record", "assert", "assert-region"];
5147
- var RUN_TAKES = [...RUN_INTENTS, "requests", "mock", "locator"];
5687
+ var RUN_TAKES = [
5688
+ ...RUN_INTENTS,
5689
+ "requests",
5690
+ "mock",
5691
+ "locator",
5692
+ "view",
5693
+ "input",
5694
+ "input-clicks",
5695
+ "pick-at",
5696
+ "agent",
5697
+ "events",
5698
+ "bind",
5699
+ "end",
5700
+ "share"
5701
+ ];
5148
5702
  var RUN_INTENT_NAMES = {
5149
5703
  pick: "Pick a locator",
5150
5704
  record: "Record here",
@@ -5155,13 +5709,23 @@ var MAX_KEPT_RESPONSES = 50;
5155
5709
  var MAX_KEPT_BODY = 256 * 1024;
5156
5710
  var MAX_REGION_SNAPSHOT = 1024 * 1024;
5157
5711
  var MAX_STOP_ERROR = 1e3;
5158
- function isObject2(value) {
5712
+ var MAX_AGENT_SNAPSHOT = 1024 * 1024;
5713
+ var MAX_SNAPSHOT_DEPTH = 100;
5714
+ var RUN_ACT_ACTIONS = ["click", "fill", "press", "check", "uncheck", "select"];
5715
+ var MAX_ACT_VALUE = 1e4;
5716
+ function isSnapshotRef(ref) {
5717
+ return typeof ref === "string" && /^(?:f\d{1,6})?e\d{1,9}$/.test(ref);
5718
+ }
5719
+ var MAX_ELEMENT_TEXT = 1e3;
5720
+ var MAX_RUN_EVENT_TEXT = 1e3;
5721
+ var MAX_RUN_EVENT_RATE = 50;
5722
+ function isObject3(value) {
5159
5723
  return typeof value === "object" && value !== null && !Array.isArray(value);
5160
5724
  }
5161
5725
  function readJson(line) {
5162
5726
  try {
5163
5727
  const value = JSON.parse(line);
5164
- return isObject2(value) ? value : null;
5728
+ return isObject3(value) ? value : null;
5165
5729
  } catch {
5166
5730
  return null;
5167
5731
  }
@@ -5173,7 +5737,7 @@ function filePath(value) {
5173
5737
  return typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0");
5174
5738
  }
5175
5739
  function readPlace(value) {
5176
- if (!isObject2(value) || !filePath(value.file) || !isInt(value.line, 1)) return null;
5740
+ if (!isObject3(value) || !filePath(value.file) || !isInt(value.line, 1)) return null;
5177
5741
  if (value.column === void 0) return { file: value.file, line: value.line };
5178
5742
  return isInt(value.column, 1) ? { file: value.file, line: value.line, column: value.column } : null;
5179
5743
  }
@@ -5188,6 +5752,18 @@ function isLocatorText(value) {
5188
5752
  return typeof value === "string" && value.length > 0 && value.length <= 1e3 && !/[\r\n\u2028\u2029]/.test(value);
5189
5753
  }
5190
5754
  var MAX_PLACES = 1e4;
5755
+ var QUERIES = [
5756
+ "requests",
5757
+ "mock",
5758
+ "locator",
5759
+ "view",
5760
+ "input",
5761
+ "pick-at",
5762
+ "snapshot",
5763
+ "act",
5764
+ "ref-locator",
5765
+ "bind"
5766
+ ];
5191
5767
  function parseHubToRun(line) {
5192
5768
  const value = readJson(line);
5193
5769
  if (!value) return null;
@@ -5195,7 +5771,9 @@ function parseHubToRun(line) {
5195
5771
  switch (value.type) {
5196
5772
  case "attached": {
5197
5773
  const holds = readHolds(value.holds);
5198
- return holds ? { type: "attached", holds } : null;
5774
+ if (!holds) return null;
5775
+ const host2 = typeof value.host === "string" && /^[A-Za-z0-9][\w '.-]{0,63}$/.test(value.host) ? value.host : null;
5776
+ return host2 ? { type: "attached", holds, host: host2 } : { type: "attached", holds };
5199
5777
  }
5200
5778
  case "breakpoints": {
5201
5779
  if (!Array.isArray(value.places) || value.places.length > MAX_PLACES) return null;
@@ -5205,7 +5783,7 @@ function parseHubToRun(line) {
5205
5783
  case "control": {
5206
5784
  const { action, stop } = value;
5207
5785
  if (action === "pause") return { type: "control", action };
5208
- if (action === "step" || action === "resume" || action === "finish") {
5786
+ if (action === "step" || action === "resume" || action === "finish" || action === "end") {
5209
5787
  return isInt(stop, 1) ? { type: "control", action, stop } : null;
5210
5788
  }
5211
5789
  return typeof action === "string" ? unknown(`control ${action}`) : null;
@@ -5219,95 +5797,331 @@ function parseHubToRun(line) {
5219
5797
  if (!isInt(request, 1)) return null;
5220
5798
  if (what === "image") return { type: "request", request, what: "image" };
5221
5799
  if (typeof what !== "string") return null;
5222
- if (!isIntent(what) && what !== "requests" && what !== "mock" && what !== "locator") {
5223
- return unknown(`request ${what}`);
5224
- }
5800
+ if (!isIntent(what) && !QUERIES.includes(what)) return unknown(`request ${what}`);
5225
5801
  if (!isInt(stop, 1)) return null;
5226
5802
  if (isIntent(what) || what === "requests") return { type: "request", request, what, stop };
5227
5803
  if (what === "mock") return isInt(value.id, 1) ? { type: "request", request, what, stop, id: value.id } : null;
5804
+ if (what === "view")
5805
+ return typeof value.on === "boolean" ? { type: "request", request, what, stop, on: value.on } : null;
5806
+ if (what === "input") {
5807
+ const events = readViewInput(value.events);
5808
+ return typeof events === "string" ? null : { type: "request", request, what, stop, events };
5809
+ }
5810
+ if (what === "pick-at") {
5811
+ const { x, y } = value;
5812
+ return isViewPoint(x, y) ? { type: "request", request, what, stop, x, y } : null;
5813
+ }
5814
+ if (what === "snapshot") {
5815
+ const { depth, image } = value;
5816
+ if (depth !== null && (!isInt(depth, 1) || depth > MAX_SNAPSHOT_DEPTH) || typeof image !== "boolean") {
5817
+ return null;
5818
+ }
5819
+ return { type: "request", request, what, stop, depth, image };
5820
+ }
5821
+ if (what === "act") {
5822
+ const act = readAct(value);
5823
+ return act ? { type: "request", request, what, stop, ...act } : null;
5824
+ }
5825
+ if (what === "ref-locator") {
5826
+ return isSnapshotRef(value.ref) ? { type: "request", request, what, stop, ref: value.ref } : null;
5827
+ }
5828
+ if (what === "bind") {
5829
+ return typeof value.on === "boolean" ? { type: "request", request, what, stop, on: value.on } : null;
5830
+ }
5228
5831
  const { locator } = value;
5229
- return locator === null || isLocatorText(locator) ? { type: "request", request, what, stop, locator } : null;
5832
+ return locator === null || isLocatorText(locator) ? { type: "request", request, what: "locator", stop, locator } : null;
5230
5833
  }
5834
+ case "events":
5835
+ return typeof value.on === "boolean" ? { type: "events", on: value.on } : null;
5836
+ case "share":
5837
+ return typeof value.on === "boolean" ? { type: "share", on: value.on } : null;
5231
5838
  default:
5232
5839
  return typeof value.type === "string" && value.type ? unknown(value.type) : null;
5233
5840
  }
5234
5841
  }
5842
+ function readAct(value) {
5843
+ if (!isObject3(value) || !isSnapshotRef(value.ref)) return null;
5844
+ const action = value.action;
5845
+ if (typeof action !== "string" || !RUN_ACT_ACTIONS.includes(action)) return null;
5846
+ const given = value.value;
5847
+ switch (action) {
5848
+ case "fill":
5849
+ return typeof given === "string" && given.length <= MAX_ACT_VALUE ? { ref: value.ref, action: "fill", value: given } : null;
5850
+ case "press":
5851
+ return isViewKeyCombo(given) ? { ref: value.ref, action: "press", value: given } : null;
5852
+ case "select":
5853
+ return typeof given === "string" && given.length <= MAX_ACT_VALUE && !/[\r\n\0]/.test(given) ? { ref: value.ref, action: "select", value: given } : null;
5854
+ default:
5855
+ return given === null || given === void 0 ? { ref: value.ref, action, value: null } : null;
5856
+ }
5857
+ }
5235
5858
 
5236
- // src/internal/capture/kept-responses.ts
5237
- var keeping = null;
5238
- var TEXT_TYPE = /^(?:text\/[\w.+-]+|application\/(?:[\w.+-]+\+)?(?:json|xml)|application\/(?:javascript|x-www-form-urlencoded|graphql))$/i;
5239
- function mimeOf(contentType) {
5240
- return (contentType ?? "").split(";")[0].trim().toLowerCase();
5859
+ // src/internal/capture/agent-page.ts
5860
+ var ACT_MS = 5e3;
5861
+ var SECRET_FIELDS = [
5862
+ 'input[type="password" i]',
5863
+ ...SECRET_AUTOCOMPLETE.flatMap((token) => [
5864
+ `input[autocomplete~="${token}" i]`,
5865
+ `textarea[autocomplete~="${token}" i]`
5866
+ ])
5867
+ ].join(", ");
5868
+ var BAR_HIDDEN = "#__piwi-pause-bar { display: none !important; }";
5869
+ var AGENT_NEEDS = "An agent reads and acts on a stopped page with Playwright 1.64 or later (page.getByRef and the AI snapshot): update @playwright/test.";
5870
+ function canActByRef(page) {
5871
+ return typeof page.getByRef === "function";
5872
+ }
5873
+ function byRef(page, ref) {
5874
+ return page.getByRef(ref);
5875
+ }
5876
+ function failed(prefix, error) {
5877
+ const first = errorMessage(error).split("\n")[0].trim();
5878
+ return hideSecretsInText(`${prefix}: ${first}`).slice(0, 300);
5879
+ }
5880
+ var MAX_FRAMES = 32;
5881
+ async function fieldSecrets(page) {
5882
+ const values = await Promise.all(
5883
+ page.frames().slice(0, MAX_FRAMES).map(
5884
+ (frame) => frame.locator(SECRET_FIELDS).evaluateAll((fields) => fields.map((field) => field.value)).catch(() => [])
5885
+ )
5886
+ );
5887
+ return [...new Set(values.flat().filter((v) => typeof v === "string" && v.length > 0))];
5888
+ }
5889
+ var VALUED_NODE = /^(\s*- [\w-]+(?: "(?:[^"\\]|\\.)*")?(?: \[[^\]]*\])*): (.+)$/;
5890
+ var ESCAPES = {
5891
+ "\\": "\\\\",
5892
+ '"': '\\"',
5893
+ "\b": "\\b",
5894
+ "\f": "\\f",
5895
+ "\n": "\\n",
5896
+ "\r": "\\r",
5897
+ " ": "\\t"
5898
+ };
5899
+ function escapedInQuotes(value) {
5900
+ let out = "";
5901
+ for (const c of value) {
5902
+ const code = c.charCodeAt(0);
5903
+ const control2 = code < 32 || code >= 127 && code <= 159;
5904
+ out += ESCAPES[c] ?? (control2 ? `\\x${code.toString(16).padStart(2, "0")}` : c);
5905
+ }
5906
+ return out;
5241
5907
  }
5242
- function textOf2(body) {
5908
+ function withoutFieldSecrets(snapshot, secrets) {
5909
+ const values = [...new Set(secrets.filter(Boolean))];
5910
+ if (!values.length) return snapshot;
5911
+ const shown2 = new Set(values.flatMap((value) => [value, `"${escapedInQuotes(value)}"`]));
5912
+ let out = snapshot.split("\n").map((line) => {
5913
+ const node = VALUED_NODE.exec(line);
5914
+ return node && shown2.has(node[2].trim()) ? `${node[1]}: ${HIDDEN_VALUE}` : line;
5915
+ }).join("\n");
5916
+ for (const value of values) {
5917
+ if (value.length < 8) continue;
5918
+ for (const written of /* @__PURE__ */ new Set([value, escapedInQuotes(value)])) out = out.split(written).join(HIDDEN_VALUE);
5919
+ }
5920
+ return out;
5921
+ }
5922
+ async function readSnapshot(page, depth, image) {
5923
+ const none = { url: "", title: "", snapshot: null, cut: false, image: null };
5924
+ if (page.isClosed()) return { ok: false, message: "The stopped page closed.", ...none };
5925
+ const url = hideSecretsInUrl(page.url()).slice(0, 8192);
5926
+ const title = hideSecretsInText(await page.title().catch(() => "")).slice(0, 1e3);
5927
+ let snapshot;
5243
5928
  try {
5244
- const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
5245
- return text.includes("\0") ? null : text;
5246
- } catch {
5247
- return null;
5929
+ snapshot = await page.ariaSnapshot({ mode: "ai", timeout: ACT_MS, ...depth === null ? {} : { depth } });
5930
+ } catch (error) {
5931
+ return { ok: false, message: failed("The stopped page could not be read", error), ...none, url, title };
5932
+ }
5933
+ const hidden2 = hideSecretsInText(withoutFieldSecrets(snapshot, await fieldSecrets(page)));
5934
+ const cut = hidden2.length > MAX_AGENT_SNAPSHOT;
5935
+ let shot = null;
5936
+ if (image) {
5937
+ const data = await page.screenshot({
5938
+ type: "jpeg",
5939
+ quality: 60,
5940
+ style: BAR_HIDDEN,
5941
+ mask: page.frames().slice(0, MAX_FRAMES).map((frame) => frame.locator(SECRET_FIELDS)),
5942
+ timeout: ACT_MS
5943
+ }).catch(() => null);
5944
+ const viewport = page.viewportSize();
5945
+ if (data) shot = { mime: "image/jpeg", data: data.toString("base64"), ...viewport };
5248
5946
  }
5947
+ return {
5948
+ ok: true,
5949
+ message: image && !shot ? "The stopped page, without an image: none could be taken." : "The stopped page.",
5950
+ url,
5951
+ title,
5952
+ snapshot: cut ? hidden2.slice(0, MAX_AGENT_SNAPSHOT) : hidden2,
5953
+ cut,
5954
+ image: shot
5955
+ };
5249
5956
  }
5250
- function startKeeping(now = Date.now()) {
5251
- keeping = { startedAt: now, kept: [], last: 0 };
5957
+ var heldIn = (el, args) => {
5958
+ const field = el;
5959
+ const tag = (field.tagName ?? "").toLowerCase();
5960
+ const control2 = tag === "input" || tag === "textarea" || tag === "select";
5961
+ const type = typeof field.type === "string" ? field.type.toLowerCase() : null;
5962
+ const secret = typeof field.matches === "function" && field.matches(args.secretFields);
5963
+ const words = typeof field.textContent === "string" ? field.textContent.replace(/\u200b/g, "").replace(/\s+/g, " ").trim() : "";
5964
+ const value = control2 && !secret && typeof field.value === "string" ? field.value : null;
5965
+ return {
5966
+ secret,
5967
+ text: words ? { read: words.slice(0, args.read), length: words.length } : null,
5968
+ value: value === null ? null : { read: value.slice(0, args.read), length: value.length },
5969
+ checked: tag === "input" && (type === "checkbox" || type === "radio") ? field.checked === true : null
5970
+ };
5971
+ };
5972
+ var READ_PAST = 1e3;
5973
+ function hiddenAndCut(held2, max) {
5974
+ const hidden2 = hideSecretsInText(held2.read);
5975
+ return { text: hidden2.slice(0, max), cut: held2.length > max || hidden2.length > max };
5976
+ }
5977
+ async function ranked(page, probe, locator, handle, testIdAttribute) {
5978
+ const { attrs, accessibleName, candidates } = await rankedCandidates(probe, locator, handle, testIdAttribute);
5979
+ const held2 = await handle.evaluate(heldIn, { secretFields: SECRET_FIELDS, read: MAX_ACT_VALUE + READ_PAST });
5980
+ const checked = [];
5981
+ for (const candidate of candidates.slice(0, MAX_CHECKED)) {
5982
+ checked.push({
5983
+ locator: candidate.locator,
5984
+ method: candidate.method,
5985
+ score: candidate.score,
5986
+ unique: await findsAlone(page, candidate.locator, handle)
5987
+ });
5988
+ }
5989
+ const locators = [...checked.filter((l) => l.unique), ...checked.filter((l) => !l.unique)];
5990
+ const { secret } = held2;
5991
+ const text = held2.text ? hiddenAndCut(held2.text, MAX_ELEMENT_TEXT) : null;
5992
+ const value = held2.value && !secret ? hiddenAndCut(held2.value, MAX_ACT_VALUE) : null;
5993
+ const role = resolveAriaRole({ ...attrs, accessibleName });
5994
+ return {
5995
+ element: {
5996
+ tagName: attrs.tagName.toLowerCase(),
5997
+ role,
5998
+ name: accessibleName ? hideSecretsInText(accessibleName).slice(0, 1e3) : null,
5999
+ testId: attrs.attributes[testIdAttribute ?? "data-testid"] ?? null,
6000
+ text: text?.text ?? null,
6001
+ textCut: text?.cut ?? false,
6002
+ value: value?.text ?? null,
6003
+ valueCut: value?.cut ?? false,
6004
+ checked: held2.checked,
6005
+ secret
6006
+ },
6007
+ locators
6008
+ };
5252
6009
  }
5253
- function stopKeeping() {
5254
- keeping = null;
6010
+ var NO_REF = (ref) => `No element of the stopped page has the ref ${ref}: take the page's snapshot again, its refs may have changed.`;
6011
+ async function handleOf(page, ref) {
6012
+ return byRef(page, ref).elementHandle({ timeout: 2e3 }).catch(() => null);
5255
6013
  }
5256
- async function keepResponse(request, response) {
5257
- const into = keeping;
5258
- if (!into || !response) return;
6014
+ async function refLocators(page, probe, ref, testIdAttribute) {
6015
+ if (!canActByRef(page)) return { ok: false, message: AGENT_NEEDS, element: null, locators: [] };
6016
+ const handle = await handleOf(page, ref);
6017
+ if (!handle) return { ok: false, message: NO_REF(ref), element: null, locators: [] };
5259
6018
  try {
5260
- const kind = request.resourceType();
5261
- if (kind !== "fetch" && kind !== "xhr") return;
5262
- const url = hideSecretsInUrl(request.url());
5263
- const method = request.method().toUpperCase();
5264
- if (url.length > 8192 || !/^[A-Z]{1,16}$/.test(method)) return;
5265
- const headers = response.headers();
5266
- const type = mimeOf(headers["content-type"]);
5267
- if (type && !TEXT_TYPE.test(type)) return;
5268
- const status = response.status();
5269
- if (status < 100 || status > 599) return;
5270
- const time = Math.max(0, Math.round(Date.now() - into.startedAt));
5271
- const size = /^\d+$/.test(headers["content-length"] ?? "") ? Number(headers["content-length"]) : null;
5272
- if (size !== null && size > MAX_KEPT_BODY) return;
5273
- into.kept.push({ id: ++into.last, method, url, status, type, size, time, response });
5274
- if (into.kept.length > MAX_KEPT_RESPONSES) into.kept.splice(0, into.kept.length - MAX_KEPT_RESPONSES);
5275
- } catch {
6019
+ const { element, locators } = await ranked(page, probe, byRef(page, ref), handle, testIdAttribute);
6020
+ const alone = locators.filter((l) => l.unique).length;
6021
+ return {
6022
+ ok: true,
6023
+ message: alone ? `${alone === 1 ? "1 locator finds" : `${alone} locators find`} that element alone.` : "No locator Piwi tried finds that element alone on the page.",
6024
+ element,
6025
+ locators
6026
+ };
6027
+ } catch (error) {
6028
+ return { ok: false, message: failed("The element could not be read", error), element: null, locators: [] };
6029
+ } finally {
6030
+ await handle.dispose().catch(() => void 0);
6031
+ }
6032
+ }
6033
+ async function play(locator, action, value) {
6034
+ const options = { timeout: ACT_MS };
6035
+ switch (action) {
6036
+ case "click":
6037
+ return locator.click(options);
6038
+ case "fill":
6039
+ return locator.fill(value ?? "", options);
6040
+ case "press":
6041
+ return locator.press(value ?? "", options);
6042
+ case "check":
6043
+ return locator.check(options);
6044
+ case "uncheck":
6045
+ return locator.uncheck(options);
6046
+ case "select":
6047
+ await locator.selectOption(value ?? "", options);
6048
+ }
6049
+ }
6050
+ function barLetsThrough(through) {
6051
+ const bar = globalThis.document?.getElementById("__piwi-pause-bar");
6052
+ if (bar) bar.style.pointerEvents = through ? "none" : "";
6053
+ }
6054
+ var PLAYED = {
6055
+ click: "Clicked",
6056
+ fill: "Filled",
6057
+ press: "Pressed a key on",
6058
+ check: "Checked",
6059
+ uncheck: "Unchecked",
6060
+ select: "Selected an option of"
6061
+ };
6062
+ async function actOnRef(page, probe, act, testIdAttribute) {
6063
+ if (!canActByRef(page)) return { ok: false, message: AGENT_NEEDS, element: null, locators: [] };
6064
+ const handle = await handleOf(page, act.ref);
6065
+ if (!handle) return { ok: false, message: NO_REF(act.ref), element: null, locators: [] };
6066
+ let before;
6067
+ try {
6068
+ before = await ranked(page, probe, byRef(page, act.ref), handle, testIdAttribute);
6069
+ } catch (error) {
6070
+ return { ok: false, message: failed("The element could not be read", error), element: null, locators: [] };
6071
+ } finally {
6072
+ await handle.dispose().catch(() => void 0);
6073
+ }
6074
+ try {
6075
+ await page.evaluate(barLetsThrough, true).catch(() => void 0);
6076
+ await play(byRef(page, act.ref), act.action, act.value);
6077
+ } catch (error) {
6078
+ return { ok: false, message: failed(`${PLAYED[act.action]} ${act.ref} failed`, error), ...before };
6079
+ } finally {
6080
+ await page.evaluate(barLetsThrough, false).catch(() => void 0);
5276
6081
  }
6082
+ return { ok: true, message: `${PLAYED[act.action]} ${act.ref}.`, ...before };
5277
6083
  }
5278
- function keptRequests() {
5279
- return (keeping?.kept ?? []).map(({ id, method, url, status, type, size, time }) => ({
5280
- id,
5281
- method,
5282
- url,
5283
- status,
5284
- type,
5285
- size,
5286
- time
5287
- }));
6084
+ function canBind(browser) {
6085
+ return typeof browser?.bind === "function";
5288
6086
  }
5289
- async function keptResponse(id) {
5290
- const found = keeping?.kept.find((kept) => kept.id === id);
5291
- if (!found) return { refusal: "The test kept no such response: it keeps the latest 50." };
5292
- let body;
6087
+ async function bindBrowser(browser, name, workspaceDir, on) {
6088
+ const bindable = browser;
6089
+ if (!bindable || !canBind(browser)) {
6090
+ return {
6091
+ ok: false,
6092
+ message: "Sharing with Playwright tools needs Playwright 1.59 or later (browser.bind): update @playwright/test.",
6093
+ name: null,
6094
+ endpoint: null
6095
+ };
6096
+ }
5293
6097
  try {
5294
- body = await internalCall(found.response, () => found.response.body());
5295
- } catch {
6098
+ if (!on) {
6099
+ await bindable.unbind?.();
6100
+ return {
6101
+ ok: true,
6102
+ message: "The browser is no longer shared with Playwright tools.",
6103
+ name: null,
6104
+ endpoint: null
6105
+ };
6106
+ }
6107
+ const { endpoint } = await bindable.bind(name, { workspaceDir });
5296
6108
  return {
5297
- refusal: "The browser no longer holds this answer's body: mock one the page received since it last loaded."
6109
+ ok: true,
6110
+ message: `The browser is shared with Playwright tools as ${name} until the test ends: any process of yours can drive it.`,
6111
+ name,
6112
+ endpoint: endpoint.slice(0, 4096)
5298
6113
  };
6114
+ } catch (error) {
6115
+ return { ok: false, message: failed("The browser could not be shared", error), name: null, endpoint: null };
5299
6116
  }
5300
- if (body.length > MAX_KEPT_BODY) return { refusal: "This answer is larger than 256 KB: Piwi mocks a smaller one." };
5301
- const text = textOf2(body);
5302
- if (text === null) return { refusal: "This answer is not text: Piwi mocks a text answer." };
5303
- return { method: found.method, url: found.url, status: found.status, type: found.type, body: text };
5304
6117
  }
5305
6118
 
5306
- // src/internal/capture/record-handoff.ts
5307
- var fs6 = __toESM(require("fs"));
5308
- var net = __toESM(require("net"));
6119
+ // src/internal/capture/run-events.ts
5309
6120
  var path9 = __toESM(require("path"));
5310
6121
 
6122
+ // src/internal/capture/run-channel.ts
6123
+ var net = __toESM(require("net"));
6124
+
5311
6125
  // ../core/src/ide-recorder.ts
5312
6126
  var IDE_RECORDER_BINDING = "__piwiRecorder";
5313
6127
  var IDE_DISPATCH_GLOBAL = "__piwiIdeDispatch";
@@ -5318,10 +6132,10 @@ function parseRecordChannel(value) {
5318
6132
  const hash = text.lastIndexOf("#");
5319
6133
  if (hash <= 0) return null;
5320
6134
  const token = text.slice(hash + 1);
5321
- const path15 = text.slice(0, hash);
5322
- return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path15.includes("\0") ? { path: path15, token } : null;
6135
+ const path16 = text.slice(0, hash);
6136
+ return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path16.includes("\0") ? { path: path16, token } : null;
5323
6137
  }
5324
- function isObject3(value) {
6138
+ function isObject4(value) {
5325
6139
  return typeof value === "object" && value !== null && !Array.isArray(value);
5326
6140
  }
5327
6141
  function parseHandoffToTest(line) {
@@ -5331,7 +6145,7 @@ function parseHandoffToTest(line) {
5331
6145
  } catch {
5332
6146
  return null;
5333
6147
  }
5334
- if (!isObject3(value)) return null;
6148
+ if (!isObject4(value)) return null;
5335
6149
  switch (value.type) {
5336
6150
  case "refused":
5337
6151
  return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
@@ -5363,205 +6177,803 @@ function lineReader(onLine, onOverflow) {
5363
6177
  };
5364
6178
  }
5365
6179
 
5366
- // src/internal/capture/record-handoff.ts
5367
- var START_WAIT_MS = 6e4;
5368
- var NEW_TAB_NOTICE = "A new tab opened: what you do there is not recorded yet. Record in the first tab.";
5369
- var relays = /* @__PURE__ */ new WeakMap();
5370
- function idleAnswer(payload) {
5371
- const request = payload ?? {};
5372
- if (request.kind === "local-get") return {};
5373
- if (request.kind !== "message") return null;
5374
- if (request.message?.type === "piwi-ping") return { ok: true };
5375
- if (request.message?.type === "piwi-session-storage") {
5376
- return request.message.op === "get" ? { ok: true, items: {} } : { ok: true };
6180
+ // src/internal/capture/kept-responses.ts
6181
+ var keeping = null;
6182
+ var TEXT_TYPE = /^(?:text\/[\w.+-]+|application\/(?:[\w.+-]+\+)?(?:json|xml)|application\/(?:javascript|x-www-form-urlencoded|graphql))$/i;
6183
+ function mimeOf(contentType) {
6184
+ return (contentType ?? "").split(";")[0].trim().toLowerCase();
6185
+ }
6186
+ function textOf2(body) {
6187
+ try {
6188
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
6189
+ return text.includes("\0") ? null : text;
6190
+ } catch {
6191
+ return null;
5377
6192
  }
5378
- return { ok: false, error: "No recording runs in this page." };
5379
6193
  }
5380
- function canRecordFromHere(channel = process.env[RECORD_CHANNEL_ENV]) {
5381
- return parseRecordChannel(channel) !== null;
6194
+ function startKeeping(now = Date.now()) {
6195
+ keeping = { startedAt: now, kept: [], last: 0 };
5382
6196
  }
5383
- function handoffPlace(location, cwd = process.cwd()) {
5384
- const m = /^(.+):(\d+):\d+$/.exec(location);
5385
- if (!m) return null;
5386
- return { file: path9.resolve(cwd, m[1]), line: Number(m[2]) };
6197
+ function stopKeeping() {
6198
+ keeping = null;
5387
6199
  }
5388
- async function recordFromHere(options) {
5389
- const channel = parseRecordChannel(options.channel ?? process.env[RECORD_CHANNEL_ENV]);
5390
- const place = handoffPlace(options.location);
5391
- if (!channel || !place) return "this run names no editor to record into.";
5392
- const { page, testInfo, ended } = options;
5393
- if (ended?.aborted) return "the test ended.";
5394
- const use = testInfo.project?.use ?? {};
5395
- const context = page.context();
5396
- return new Promise((resolve5) => {
5397
- const socket2 = net.connect(channel.path);
5398
- socket2.setEncoding("utf8");
5399
- const answers = /* @__PURE__ */ new Map();
5400
- let calls = 0;
5401
- let attached = false;
5402
- let recording = false;
5403
- let settled = false;
5404
- let initScript;
5405
- const send = (message) => {
5406
- if (!socket2.destroyed) socket2.write(`${JSON.stringify(message)}
5407
- `);
5408
- };
5409
- const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
5410
- const onGone = () => {
5411
- if (attached) send({ type: "closed" });
5412
- finish("the page closed.");
6200
+ async function keepResponse(request, response) {
6201
+ const into = keeping;
6202
+ if (!into || !response) return;
6203
+ try {
6204
+ const kind = request.resourceType();
6205
+ if (kind !== "fetch" && kind !== "xhr") return;
6206
+ const url = hideSecretsInUrl(request.url());
6207
+ const method = request.method().toUpperCase();
6208
+ if (url.length > 8192 || !/^[A-Z]{1,16}$/.test(method)) return;
6209
+ const headers = response.headers();
6210
+ const type = mimeOf(headers["content-type"]);
6211
+ if (type && !TEXT_TYPE.test(type)) return;
6212
+ const status = response.status();
6213
+ if (status < 100 || status > 599) return;
6214
+ const time = Math.max(0, Math.round(Date.now() - into.startedAt));
6215
+ const size = /^\d+$/.test(headers["content-length"] ?? "") ? Number(headers["content-length"]) : null;
6216
+ if (size !== null && size > MAX_KEPT_BODY) return;
6217
+ into.kept.push({ id: ++into.last, method, url, status, type, size, time, response });
6218
+ if (into.kept.length > MAX_KEPT_RESPONSES) into.kept.splice(0, into.kept.length - MAX_KEPT_RESPONSES);
6219
+ } catch {
6220
+ }
6221
+ }
6222
+ function keptRequests() {
6223
+ return (keeping?.kept ?? []).map(({ id, method, url, status, type, size, time }) => ({
6224
+ id,
6225
+ method,
6226
+ url,
6227
+ status,
6228
+ type,
6229
+ size,
6230
+ time
6231
+ }));
6232
+ }
6233
+ async function keptResponse(id) {
6234
+ const found = keeping?.kept.find((kept) => kept.id === id);
6235
+ if (!found) return { refusal: "The test kept no such response: it keeps the latest 50." };
6236
+ let body;
6237
+ try {
6238
+ body = await internalCall(found.response, () => found.response.body());
6239
+ } catch {
6240
+ return {
6241
+ refusal: "The browser no longer holds this answer's body: mock one the page received since it last loaded."
5413
6242
  };
5414
- const onEnded = () => {
5415
- try {
5416
- if (recording) {
5417
- void internalCall(
5418
- page,
5419
- () => page.evaluate(
5420
- ([name, value]) => {
5421
- const dispatch = globalThis[name];
5422
- if (typeof dispatch === "function") dispatch(value);
5423
- },
5424
- [IDE_DISPATCH_GLOBAL, { type: "piwi-recording-stopped" }]
5425
- )
5426
- ).catch(() => void 0);
6243
+ }
6244
+ if (body.length > MAX_KEPT_BODY) return { refusal: "This answer is larger than 256 KB: Piwi mocks a smaller one." };
6245
+ const text = textOf2(body);
6246
+ if (text === null) return { refusal: "This answer is not text: Piwi mocks a text answer." };
6247
+ return { method: found.method, url: found.url, status: found.status, type: found.type, body: text };
6248
+ }
6249
+
6250
+ // src/internal/capture/live-view.ts
6251
+ var http = __toESM(require("http"));
6252
+ var https = __toESM(require("https"));
6253
+
6254
+ // ../core/src/live-view.ts
6255
+ var LIVE_VIEW_ENV = "PIWI_LIVE_VIEW";
6256
+ var LIVE_FRAME_WIDTH = 640;
6257
+ var LIVE_FRAME_HEIGHT = 400;
6258
+ var LIVE_FRAME_QUALITY = 50;
6259
+ var LIVE_FRAME_FPS = 4;
6260
+ var LIVE_FRAME_BYTE_RATE = 256 * 1024;
6261
+ var MAX_LIVE_FRAME = 192 * 1024;
6262
+ var MAX_LIVE_FRAME_BASE64 = 4 * Math.ceil(MAX_LIVE_FRAME / 3);
6263
+ var MAX_LIVE_MESSAGE = MAX_LIVE_FRAME_BASE64 + 8 * 1024;
6264
+ var KEY = /^pdl_[A-Za-z0-9_-]{43}$/;
6265
+ function isLiveViewKey(value) {
6266
+ return typeof value === "string" && KEY.test(value);
6267
+ }
6268
+ function parseLiveViewEnv(value) {
6269
+ if (!value || value.length > 4096) return null;
6270
+ const hash = value.lastIndexOf("#");
6271
+ if (hash < 0) return null;
6272
+ const key = value.slice(hash + 1);
6273
+ if (!isLiveViewKey(key)) return null;
6274
+ let url;
6275
+ try {
6276
+ url = new URL(value.slice(0, hash));
6277
+ } catch {
6278
+ return null;
6279
+ }
6280
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
6281
+ if (url.search || url.username || url.password) return null;
6282
+ const run = /\/api\/test-runs\/(\d{1,15})\/live-view$/.exec(url.pathname);
6283
+ if (!run) return null;
6284
+ const runId = Number(run[1]);
6285
+ if (!Number.isSafeInteger(runId) || runId < 1) return null;
6286
+ return { url: url.toString(), runId, key };
6287
+ }
6288
+ function liveRunPage(address) {
6289
+ const url = new URL(address.url);
6290
+ url.pathname = url.pathname.replace(/\/api\/test-runs\/\d+\/live-view$/, `/test-runs/${address.runId}`);
6291
+ url.hash = "";
6292
+ return url.toString();
6293
+ }
6294
+ var MISSING = ["no-instance", "unregistered", "off", "local"];
6295
+ function parseLiveViewMissing(value) {
6296
+ const why = value?.startsWith("none:") ? value.slice(5) : null;
6297
+ return why && MISSING.includes(why) ? why : null;
6298
+ }
6299
+ var LIVE_SHARE_MISSING = {
6300
+ "no-instance": "This run cannot be shared live: it reports to no Piwi instance. Name your team's instance in PIWI_DASHBOARD_URL.",
6301
+ unregistered: "This run cannot be shared live: the reporter's global setup did not register it with the instance as it started (wrapConfig adds it).",
6302
+ off: "This run cannot be shared live: its project's live view is off on the instance. A Project admin turns it on in Project \u2192 Settings \u2192 Live view.",
6303
+ local: "This run cannot be shared live: it reports to the Piwi desktop app on this machine, which no teammate reaches. Name your team's instance in PIWI_DASHBOARD_URL to share a run there."
6304
+ };
6305
+ var LIVE_SHARE_UNKNOWN = "This run cannot be shared live: the Piwi reporter gave its workers no live view key.";
6306
+ function liveShareRefusal(status) {
6307
+ switch (status) {
6308
+ case 404:
6309
+ return "Sharing stopped: the project's live view was turned off on the instance.";
6310
+ case 401:
6311
+ case 403:
6312
+ return "Sharing stopped: the instance no longer takes this run's live view key (it started again, or gave the key up).";
6313
+ case 410:
6314
+ return "Sharing stopped: the run is no longer live on the instance.";
6315
+ case 409:
6316
+ return "Sharing stopped: the instance holds the run as interrupted, having heard nothing from it for a while. Share the run again once it reports there again.";
6317
+ default:
6318
+ return "Sharing stopped: the instance did not answer. Share the run again to try once more.";
6319
+ }
6320
+ }
6321
+ function isObject5(value) {
6322
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6323
+ }
6324
+ function isInt2(value, min, max) {
6325
+ return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
6326
+ }
6327
+ function parseInstanceToWorker(data) {
6328
+ let value;
6329
+ try {
6330
+ value = JSON.parse(data);
6331
+ } catch {
6332
+ return null;
6333
+ }
6334
+ if (!isObject5(value) || value.type !== "viewers" || !isInt2(value.count, 0, 1e6)) return null;
6335
+ return { type: "viewers", count: value.count };
6336
+ }
6337
+
6338
+ // src/internal/support/shard-info.ts
6339
+ function parseShardSpec(raw) {
6340
+ const match = raw?.trim().match(/^(\d+)\s*\/\s*(\d+)$/);
6341
+ if (!match) return null;
6342
+ const current = Number(match[1]);
6343
+ const total = Number(match[2]);
6344
+ return current >= 1 && current <= total && total > 1 ? { current, total } : null;
6345
+ }
6346
+ function resolveShardInfo(config, env = process.env) {
6347
+ const pwShard = config.shard;
6348
+ if (pwShard && pwShard.total > 1) return { current: pwShard.current, total: pwShard.total };
6349
+ return parseShardSpec(env[PIWI_SHARD_ENV]);
6350
+ }
6351
+
6352
+ // src/internal/capture/page-view.ts
6353
+ var HOLDERS = /* @__PURE__ */ new WeakMap();
6354
+ var TEST_SCREENCASTS = /* @__PURE__ */ new WeakSet();
6355
+ var OWN_SCREENCASTS = /* @__PURE__ */ new WeakMap();
6356
+ function screencastOf(page) {
6357
+ const seen = page.screencast;
6358
+ return (seen && OWN_SCREENCASTS.get(seen)) ?? seen;
6359
+ }
6360
+ function yieldToTest(page) {
6361
+ TEST_SCREENCASTS.add(page);
6362
+ const holders = HOLDERS.get(page);
6363
+ if (!holders?.size) return null;
6364
+ return Promise.all([...holders].map((holder) => holder.release()));
6365
+ }
6366
+ function guardScreencast(page) {
6367
+ const own = page.screencast;
6368
+ if (!own || typeof own !== "object" || OWN_SCREENCASTS.has(own)) return;
6369
+ const guarded = new Proxy(own, {
6370
+ get(target, prop) {
6371
+ const value = Reflect.get(target, prop);
6372
+ if (typeof value !== "function") return value;
6373
+ if (prop !== "start" && prop !== "stop") return (...args) => callMethod(target, prop, args);
6374
+ return (...args) => {
6375
+ const yielded = yieldToTest(page);
6376
+ return yielded ? yielded.then(() => callMethod(target, prop, args)) : callMethod(target, prop, args);
6377
+ };
6378
+ }
6379
+ });
6380
+ OWN_SCREENCASTS.set(guarded, own);
6381
+ try {
6382
+ page.screencast = guarded;
6383
+ } catch {
6384
+ }
6385
+ }
6386
+ async function viewportOf(page) {
6387
+ const size = page.viewportSize();
6388
+ if (size) return size;
6389
+ const read = await internalCall(
6390
+ page,
6391
+ () => page.evaluate(() => {
6392
+ const { innerWidth, innerHeight } = globalThis;
6393
+ return { width: innerWidth, height: innerHeight };
6394
+ })
6395
+ ).catch(() => null);
6396
+ return read && isViewSize(read.width, read.height) ? read : null;
6397
+ }
6398
+ async function openPageView(page, options) {
6399
+ const viewport = await viewportOf(page);
6400
+ if (!viewport) return null;
6401
+ let closed = false;
6402
+ let last = null;
6403
+ let sent2 = null;
6404
+ let refused = false;
6405
+ const interval = 1e3 / options.fps;
6406
+ const pacer = new LatestPacer({
6407
+ intervalMs: interval,
6408
+ ...options.spacing ? { spacing: options.spacing } : {},
6409
+ busy: options.busy,
6410
+ send: (frame) => {
6411
+ if (!options.live()) return;
6412
+ refused = false;
6413
+ sent2 = frame;
6414
+ options.send(frame);
6415
+ }
6416
+ });
6417
+ let lighter = null;
6418
+ let lighterAt = -Infinity;
6419
+ const lighten = () => {
6420
+ const now = Date.now();
6421
+ if (lighter || now - lighterAt < options.shotMs) return;
6422
+ lighterAt = now;
6423
+ lighter = internalCall(
6424
+ page,
6425
+ () => page.screenshot({ type: "jpeg", quality: options.lightQuality, scale: "css", caret: "initial", timeout: 5e3 })
6426
+ ).then(
6427
+ (shot) => {
6428
+ if (closed) return;
6429
+ if (shot.length <= options.maxFrameBytes) return pacer.offer({ data: shot, ...viewport });
6430
+ if (refused || !options.live()) return;
6431
+ refused = true;
6432
+ sent2 = "refused";
6433
+ options.refuse();
6434
+ },
6435
+ () => void 0
6436
+ ).finally(() => {
6437
+ lighter = null;
6438
+ });
6439
+ };
6440
+ const onFrame = (data, width, height) => {
6441
+ if (closed || last?.equals(data)) return;
6442
+ last = data;
6443
+ if (data.length > options.maxFrameBytes) return lighten();
6444
+ const named = isViewSize(width, height);
6445
+ pacer.offer({ data, width: named ? width : viewport.width, height: named ? height : viewport.height });
6446
+ };
6447
+ const screencast = screencastOf(page);
6448
+ let end = null;
6449
+ let shots = null;
6450
+ let shooting = false;
6451
+ const shoot = async () => {
6452
+ shots = null;
6453
+ if (closed || page.isClosed()) return;
6454
+ const shot = await internalCall(
6455
+ page,
6456
+ () => page.screenshot({ type: "jpeg", quality: options.quality, scale: "css", caret: "initial", timeout: 5e3 })
6457
+ ).catch(() => null);
6458
+ if (shot) onFrame(shot, viewport.width, viewport.height);
6459
+ if (!closed) {
6460
+ shots = setTimeout(() => void shoot(), options.shotMs);
6461
+ shots.unref?.();
6462
+ }
6463
+ };
6464
+ const takeShots = () => {
6465
+ if (shooting || closed) return;
6466
+ shooting = true;
6467
+ void shoot();
6468
+ };
6469
+ const acks = new FrameAcks(interval);
6470
+ const holders = HOLDERS.get(page) ?? /* @__PURE__ */ new Set();
6471
+ HOLDERS.set(page, holders);
6472
+ let starting = null;
6473
+ const holder = {
6474
+ release: async () => {
6475
+ holders.delete(holder);
6476
+ await starting?.catch(() => void 0);
6477
+ const stop = end;
6478
+ end = null;
6479
+ if (stop && !page.isClosed()) await stop().catch(() => void 0);
6480
+ takeShots();
6481
+ }
6482
+ };
6483
+ if (typeof screencast?.start === "function" && !TEST_SCREENCASTS.has(page)) {
6484
+ holders.add(holder);
6485
+ starting = internalCall(
6486
+ page,
6487
+ () => screencast.start({
6488
+ size: options.size(viewport),
6489
+ quality: options.quality,
6490
+ onFrame: (frame) => {
6491
+ onFrame(frame.data, frame.viewportWidth, frame.viewportHeight);
6492
+ return acks.next();
5427
6493
  }
5428
- if (attached) send({ type: "closed" });
5429
- finish("the test ended.");
5430
- } catch {
5431
- }
5432
- };
5433
- page.once("close", onGone);
5434
- page.once("crash", onGone);
5435
- ended?.addEventListener("abort", onEnded, { once: true });
5436
- const startWait = setTimeout(() => finish("the editor did not start the recording."), START_WAIT_MS);
5437
- const release = () => {
5438
- const relay = relays.get(context);
5439
- if (relay && relay.recording === relayed) relay.recording = null;
5440
- void Promise.resolve(initScript?.dispose?.()).catch(() => void 0);
5441
- initScript = void 0;
5442
- };
5443
- const finish = (said) => {
5444
- if (settled) return;
5445
- settled = true;
5446
- clearTimeout(startWait);
5447
- release();
5448
- for (const [call, answer] of answers) {
5449
- answers.delete(call);
5450
- answer(null);
5451
- }
5452
- context.off("page", onPage);
5453
- page.off("close", onGone);
5454
- page.off("crash", onGone);
5455
- ended?.removeEventListener("abort", onEnded);
5456
- socket2.end();
5457
- resolve5(recording ? null : said);
5458
- };
5459
- const relayed = (source, payload) => new Promise((answer) => {
5460
- const call = ++calls;
5461
- answers.set(call, answer);
5462
- const outside = source.page !== page || source.frame !== page.mainFrame();
5463
- send({ type: "binding", call, outside, payload });
6494
+ })
6495
+ );
6496
+ try {
6497
+ const started = await starting;
6498
+ end = () => internalCall(page, async () => {
6499
+ if (typeof started?.dispose === "function") await started.dispose();
6500
+ else await screencast.stop?.();
6501
+ });
6502
+ } catch {
6503
+ holders.delete(holder);
6504
+ takeShots();
6505
+ }
6506
+ } else {
6507
+ takeShots();
6508
+ }
6509
+ return {
6510
+ get screencast() {
6511
+ return end !== null;
6512
+ },
6513
+ resend: () => {
6514
+ if (closed || !sent2 || !options.live()) return;
6515
+ if (sent2 === "refused") options.refuse();
6516
+ else options.send(sent2);
6517
+ },
6518
+ close: async () => {
6519
+ closed = true;
6520
+ sent2 = null;
6521
+ acks.close();
6522
+ pacer.close();
6523
+ if (shots) clearTimeout(shots);
6524
+ holders.delete(holder);
6525
+ await lighter;
6526
+ const stop = end;
6527
+ end = null;
6528
+ if (stop && !page.isClosed()) await stop().catch(() => void 0);
6529
+ }
6530
+ };
6531
+ }
6532
+
6533
+ // src/internal/capture/live-view.ts
6534
+ var RETRY_MS = [1e3, 2e3, 4e3, 8e3, 16e3];
6535
+ var MAX_RECONNECTS = 20;
6536
+ var STEADY_MS = 3e4;
6537
+ var STREAM_IDLE_MS = 45e3;
6538
+ var POST_TIMEOUT_MS = 1e4;
6539
+ var MAX_EVENT_TEXT = 64 * 1024;
6540
+ var MAX_STATES_WAITING = 8;
6541
+ var LIGHT_QUALITY = 30;
6542
+ var SHOT_MS = 1e3;
6543
+ var VIEW_END_MS = 1e3;
6544
+ var GIVE_UP = /* @__PURE__ */ new Set([401, 403, 404, 410]);
6545
+ var TOO_HEAVY = `This page is too heavy for the live view: each image of it is over ${MAX_LIVE_FRAME / 1024} KB, a lighter one included.`;
6546
+ var NO_PAGE = "This test has no page to show yet.";
6547
+ var NO_TEST = "No test runs on this worker now: the next one shows once it opens its page.";
6548
+ var link = null;
6549
+ var missing = null;
6550
+ var changes = /* @__PURE__ */ new Set();
6551
+ var shareTurn = Promise.resolve();
6552
+ var running = null;
6553
+ var view = null;
6554
+ var hidden = null;
6555
+ function hideKey(env) {
6556
+ const value = env[LIVE_VIEW_ENV];
6557
+ if (value === void 0 || hidden) return;
6558
+ delete env[LIVE_VIEW_ENV];
6559
+ const onStop = (message) => {
6560
+ if (message?.method !== "__stop__") return;
6561
+ env[LIVE_VIEW_ENV] = value;
6562
+ process.nextTick(() => {
6563
+ if (hidden?.onStop === onStop) delete env[LIVE_VIEW_ENV];
5464
6564
  });
5465
- const load = async (bundle) => {
5466
- if (!/^https?:/i.test(page.url())) {
5467
- const why = `the recorder records http(s) pages only, and this page is ${page.url() || "blank"}.`;
5468
- send({ type: "notice", message: `Nothing can be recorded here: ${why}` });
5469
- finish(why);
5470
- return;
5471
- }
5472
- let relay = relays.get(context);
5473
- if (!relay) {
5474
- const created = { recording: null, lingering: false };
5475
- await context.exposeBinding(
5476
- IDE_RECORDER_BINDING,
5477
- (source, payload) => created.recording ? created.recording(source, payload) : idleAnswer(payload)
5478
- );
5479
- relays.set(context, created);
5480
- relay = created;
5481
- }
5482
- if (settled) return;
5483
- relay.recording = relayed;
5484
- if (!relay.lingering) {
5485
- initScript = await context.addInitScript({ path: bundle });
5486
- if (typeof initScript?.dispose !== "function") relay.lingering = true;
6565
+ };
6566
+ process.prependListener("message", onStop);
6567
+ hidden = { env, value, onStop };
6568
+ }
6569
+ function showKey() {
6570
+ const own = hidden;
6571
+ hidden = null;
6572
+ if (!own) return;
6573
+ process.off("message", own.onStop);
6574
+ own.env[LIVE_VIEW_ENV] = own.value;
6575
+ }
6576
+ function liveViewOpen() {
6577
+ return link !== null;
6578
+ }
6579
+ function active(own) {
6580
+ return !own.ended && own.refused === null && own.sharing;
6581
+ }
6582
+ function changed() {
6583
+ for (const listener of changes) listener();
6584
+ }
6585
+ function onLiveViewChange(listener) {
6586
+ changes.add(listener);
6587
+ return () => changes.delete(listener);
6588
+ }
6589
+ function liveViewState() {
6590
+ const own = link;
6591
+ if (!own) return { can: false, on: false, viewers: 0, page: null, why: missing ?? LIVE_SHARE_UNKNOWN };
6592
+ const page = liveRunPage(own.address);
6593
+ if (own.refused) return { can: false, on: false, viewers: 0, page, why: own.refused };
6594
+ const on = active(own);
6595
+ return { can: true, on, viewers: on ? own.viewers : 0, page, why: own.lost };
6596
+ }
6597
+ function openLiveView(worker, env = process.env, ci = detectCi(env)) {
6598
+ if (link) return false;
6599
+ const value = env[LIVE_VIEW_ENV];
6600
+ const address = parseLiveViewEnv(value);
6601
+ hideKey(env);
6602
+ if (!address) {
6603
+ const why = parseLiveViewMissing(value);
6604
+ missing = why ? LIVE_SHARE_MISSING[why] : LIVE_SHARE_UNKNOWN;
6605
+ changed();
6606
+ return false;
6607
+ }
6608
+ missing = null;
6609
+ const secure = address.url.startsWith("https:");
6610
+ const client = secure ? https : http;
6611
+ link = {
6612
+ address,
6613
+ id: { shard: resolveShardInfo(worker.config, env)?.current ?? null, worker: worker.workerIndex },
6614
+ client,
6615
+ agent: new client.Agent({ keepAlive: true, maxSockets: 2 }),
6616
+ stream: null,
6617
+ timer: null,
6618
+ retries: 0,
6619
+ reconnects: 0,
6620
+ ended: false,
6621
+ refused: null,
6622
+ sharing: ci,
6623
+ lost: null,
6624
+ answered: null,
6625
+ viewers: 0,
6626
+ states: Promise.resolve(),
6627
+ statesWaiting: 0,
6628
+ stop: null,
6629
+ posting: false
6630
+ };
6631
+ if (ci) connect(link);
6632
+ changed();
6633
+ return true;
6634
+ }
6635
+ function closeLiveView() {
6636
+ const own = link;
6637
+ link = null;
6638
+ running = null;
6639
+ missing = null;
6640
+ showKey();
6641
+ void stopView();
6642
+ if (!own) return;
6643
+ own.ended = true;
6644
+ if (own.timer) clearTimeout(own.timer);
6645
+ own.stream?.destroy();
6646
+ own.agent.destroy();
6647
+ }
6648
+ function shareLiveView(on) {
6649
+ const turn = shareTurn.then(() => on ? startSharing() : stopSharing());
6650
+ shareTurn = turn.catch(() => void 0);
6651
+ return turn;
6652
+ }
6653
+ function startSharing() {
6654
+ const own = link;
6655
+ if (!own || own.ended || own.refused || own.sharing) return;
6656
+ own.sharing = true;
6657
+ own.lost = null;
6658
+ own.answered = null;
6659
+ own.retries = 0;
6660
+ own.reconnects = 0;
6661
+ connect(own);
6662
+ changed();
6663
+ }
6664
+ async function stopSharing() {
6665
+ const own = link;
6666
+ if (!own || own.ended || !own.sharing) return;
6667
+ const streamed = own.stream !== null && own.refused === null;
6668
+ own.sharing = false;
6669
+ own.lost = null;
6670
+ if (own.timer) clearTimeout(own.timer);
6671
+ own.timer = null;
6672
+ await stopView();
6673
+ if (streamed) await send(own, { type: "unshared", ...own.id });
6674
+ own.stream?.destroy();
6675
+ own.stream = null;
6676
+ own.viewers = 0;
6677
+ changed();
6678
+ }
6679
+ function liveViewTestBegan(title, call) {
6680
+ if (!link) return;
6681
+ running = { title, page: null, call };
6682
+ }
6683
+ function liveViewPage(page) {
6684
+ if (!link || !running || running.page) return;
6685
+ running.page = page;
6686
+ if (link.viewers > 0 && active(link)) startView();
6687
+ }
6688
+ async function liveViewTestEnded() {
6689
+ if (!running) return;
6690
+ running = null;
6691
+ await stopView();
6692
+ const own = link;
6693
+ if (own && own.viewers > 0 && active(own)) void post(own, { type: "ended", ...own.id });
6694
+ }
6695
+ function liveViewStopped(stop) {
6696
+ const own = link;
6697
+ if (!own || own.ended) return () => void 0;
6698
+ const shown2 = { ...stop, call: hideCallSecrets(stop.call) };
6699
+ own.stop = shown2;
6700
+ postState(own, { type: "stop", ...own.id, stop: shown2 });
6701
+ return () => {
6702
+ if (own.stop === shown2) own.stop = null;
6703
+ postState(own, { type: "stop", ...own.id, stop: null });
6704
+ };
6705
+ }
6706
+ function hideCallSecrets(call) {
6707
+ return { ...call, title: hideSecretsInText(call.title) };
6708
+ }
6709
+ function postState(own, message) {
6710
+ if (!active(own) || own.statesWaiting >= MAX_STATES_WAITING) return;
6711
+ own.statesWaiting++;
6712
+ own.states = own.states.then(() => post(own, message)).finally(() => {
6713
+ own.statesWaiting--;
6714
+ });
6715
+ }
6716
+ function connect(own) {
6717
+ if (!active(own)) return;
6718
+ const url = new URL(own.address.url);
6719
+ url.searchParams.set("worker", String(own.id.worker));
6720
+ if (own.id.shard !== null) url.searchParams.set("shard", String(own.id.shard));
6721
+ let openedAt = null;
6722
+ let dropped = false;
6723
+ const drop = (status) => {
6724
+ if (dropped) return;
6725
+ dropped = true;
6726
+ if (own.stream !== request) return;
6727
+ own.stream = null;
6728
+ setViewers(own, 0);
6729
+ if (!active(own)) return;
6730
+ if (openedAt !== null && Date.now() - openedAt >= STEADY_MS) own.retries = 0;
6731
+ if (status !== null) own.answered = status;
6732
+ if (status !== null && GIVE_UP.has(status)) return void refuse(own, status);
6733
+ if (own.retries >= RETRY_MS.length || own.reconnects >= MAX_RECONNECTS) return void giveUp(own);
6734
+ const wait = RETRY_MS[own.retries++];
6735
+ own.reconnects++;
6736
+ own.timer = setTimeout(() => {
6737
+ own.timer = null;
6738
+ connect(own);
6739
+ }, wait);
6740
+ own.timer.unref?.();
6741
+ };
6742
+ const request = own.client.request(url, {
6743
+ method: "GET",
6744
+ agent: own.agent,
6745
+ headers: {
6746
+ Accept: "text/event-stream",
6747
+ Authorization: `Bearer ${own.address.key}`,
6748
+ "Cache-Control": "no-cache"
6749
+ }
6750
+ });
6751
+ own.stream = request;
6752
+ request.on("socket", (socket2) => socket2.unref());
6753
+ request.setTimeout(STREAM_IDLE_MS, () => request.destroy());
6754
+ request.on("error", () => drop(null));
6755
+ request.on("close", () => drop(null));
6756
+ request.on("response", (response) => {
6757
+ if (response.statusCode !== 200) {
6758
+ response.resume();
6759
+ return drop(response.statusCode ?? null);
6760
+ }
6761
+ openedAt = Date.now();
6762
+ own.answered = null;
6763
+ if (own.stop) postState(own, { type: "stop", ...own.id, stop: own.stop });
6764
+ response.setEncoding("utf8");
6765
+ let text = "";
6766
+ response.on("data", (chunk) => {
6767
+ text += chunk;
6768
+ if (text.length > MAX_EVENT_TEXT && !text.includes("\n\n")) return void request.destroy();
6769
+ let end;
6770
+ while ((end = text.indexOf("\n\n")) >= 0) {
6771
+ const block = text.slice(0, end);
6772
+ text = text.slice(end + 2);
6773
+ const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
6774
+ const message = data ? parseInstanceToWorker(data) : null;
6775
+ if (message) setViewers(own, message.count);
5487
6776
  }
5488
- if (settled) return release();
5489
- await page.evaluate(fs6.readFileSync(bundle, "utf-8"));
5490
- if (settled) return release();
5491
- recording = true;
5492
- clearTimeout(startWait);
5493
- context.on("page", onPage);
5494
- send({ type: "loaded" });
5495
- };
5496
- socket2.on("connect", () => {
5497
- attached = true;
5498
- send({
5499
- type: "attach",
5500
- token: channel.token,
5501
- file: place.file,
5502
- line: place.line,
5503
- reason: options.reason,
5504
- testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
5505
- baseURL: typeof use.baseURL === "string" ? use.baseURL : null,
5506
- ...options.mode === "check" ? { mode: "check", stop: options.stop ?? null } : {}
5507
- });
5508
6777
  });
5509
- socket2.on(
5510
- "data",
5511
- lineReader(
5512
- (line) => {
5513
- const message = parseHandoffToTest(line);
5514
- if (!message || settled) return;
5515
- switch (message.type) {
5516
- case "refused":
5517
- return finish(message.message);
5518
- case "load":
5519
- return void load(message.bundle).catch(
5520
- (error) => finish(`the recorder could not be loaded: ${error instanceof Error ? error.message : String(error)}`)
5521
- );
5522
- case "answer": {
5523
- const answer = answers.get(message.call);
5524
- answers.delete(message.call);
5525
- return answer?.(message.value);
5526
- }
5527
- case "dispatch":
5528
- return void page.evaluate(
5529
- ([name, value]) => {
5530
- const dispatch = globalThis[name];
5531
- if (typeof dispatch === "function") dispatch(value);
5532
- },
5533
- [IDE_DISPATCH_GLOBAL, message.message]
5534
- ).catch(() => void 0);
5535
- case "stop":
5536
- return finish(null);
5537
- }
5538
- },
5539
- () => finish("the editor sent a message too long to read.")
5540
- )
5541
- );
5542
- socket2.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
5543
- socket2.on("close", () => finish("the editor closed the recording."));
6778
+ response.on("error", () => drop(null));
6779
+ response.on("close", () => drop(null));
6780
+ });
6781
+ request.end();
6782
+ }
6783
+ function refuse(own, status) {
6784
+ if (own.refused !== null) return;
6785
+ own.refused = liveShareRefusal(status);
6786
+ own.viewers = 0;
6787
+ own.stream?.destroy();
6788
+ own.agent.destroy();
6789
+ void stopView();
6790
+ changed();
6791
+ }
6792
+ function giveUp(own) {
6793
+ own.sharing = false;
6794
+ own.lost = liveShareRefusal(own.answered);
6795
+ own.viewers = 0;
6796
+ if (own.timer) clearTimeout(own.timer);
6797
+ own.timer = null;
6798
+ own.stream?.destroy();
6799
+ own.stream = null;
6800
+ void stopView();
6801
+ changed();
6802
+ }
6803
+ function setViewers(own, count) {
6804
+ const before = own.viewers;
6805
+ own.viewers = count;
6806
+ if (own !== link) return;
6807
+ if (count !== before) changed();
6808
+ if (count === 0) return void stopView();
6809
+ if (!active(own)) return;
6810
+ if (!running) {
6811
+ if (count > before) void post(own, { type: "frame", ...own.id, refused: NO_TEST });
6812
+ return;
6813
+ }
6814
+ if (view) {
6815
+ if (count > before) void view.opened.then((opened) => opened?.resend());
6816
+ return;
6817
+ }
6818
+ startView();
6819
+ }
6820
+ function startView() {
6821
+ const own = link;
6822
+ const page = running?.page ?? null;
6823
+ if (!own || !active(own) || view) return;
6824
+ if (!page || page.isClosed()) {
6825
+ void post(own, { type: "frame", ...own.id, refused: NO_PAGE });
6826
+ return;
6827
+ }
6828
+ const live = () => view?.page === page && link === own && active(own) && own.viewers > 0;
6829
+ const opened = openPageView(page, {
6830
+ size: (viewport) => fitViewSize(viewport, LIVE_FRAME_WIDTH, LIVE_FRAME_HEIGHT),
6831
+ quality: LIVE_FRAME_QUALITY,
6832
+ fps: LIVE_FRAME_FPS,
6833
+ spacing: (frame) => frame.data.length * 1e3 / LIVE_FRAME_BYTE_RATE,
6834
+ maxFrameBytes: MAX_LIVE_FRAME,
6835
+ lightQuality: LIGHT_QUALITY,
6836
+ shotMs: SHOT_MS,
6837
+ busy: () => own.posting,
6838
+ live,
6839
+ send: (frame) => postFrame(own, frame),
6840
+ refuse: () => void post(own, { type: "frame", ...own.id, refused: TOO_HEAVY })
6841
+ }).catch(() => null);
6842
+ const onClose = () => {
6843
+ if (view?.page === page) void stopView();
6844
+ };
6845
+ page.once("close", onClose);
6846
+ view = { page, opened, onClose };
6847
+ }
6848
+ async function stopView() {
6849
+ const shown2 = view;
6850
+ view = null;
6851
+ if (!shown2) return;
6852
+ shown2.page.off("close", shown2.onClose);
6853
+ const closed = shown2.opened.then((opened) => opened?.close());
6854
+ let timer;
6855
+ await Promise.race([
6856
+ closed.catch(() => void 0),
6857
+ new Promise((resolve5) => {
6858
+ timer = setTimeout(resolve5, VIEW_END_MS);
6859
+ timer.unref?.();
6860
+ })
6861
+ ]);
6862
+ clearTimeout(timer);
6863
+ }
6864
+ function postFrame(own, frame) {
6865
+ if (!active(own)) return;
6866
+ own.posting = true;
6867
+ const call = running?.call() ?? null;
6868
+ const data = frame.data.toString("base64");
6869
+ void post(own, {
6870
+ type: "frame",
6871
+ ...own.id,
6872
+ data,
6873
+ call: call && hideCallSecrets(call),
6874
+ test: running?.title ?? null
6875
+ }).catch(() => void 0).finally(() => {
6876
+ own.posting = false;
6877
+ });
6878
+ }
6879
+ function post(own, message) {
6880
+ return active(own) ? send(own, message) : Promise.resolve(null);
6881
+ }
6882
+ function send(own, message) {
6883
+ if (own.ended || own.refused !== null) return Promise.resolve(null);
6884
+ const body = JSON.stringify(message);
6885
+ return new Promise((resolve5) => {
6886
+ let settled = false;
6887
+ const settle = (status) => {
6888
+ if (settled) return;
6889
+ settled = true;
6890
+ if (status !== null && GIVE_UP.has(status)) refuse(own, status);
6891
+ resolve5(status);
6892
+ };
6893
+ const request = own.client.request(own.address.url, {
6894
+ method: "POST",
6895
+ agent: own.agent,
6896
+ headers: {
6897
+ "Content-Type": "application/json",
6898
+ "Content-Length": Buffer.byteLength(body),
6899
+ Authorization: `Bearer ${own.address.key}`
6900
+ }
6901
+ });
6902
+ request.on("socket", (socket2) => socket2.unref());
6903
+ request.setTimeout(POST_TIMEOUT_MS, () => request.destroy());
6904
+ request.on("error", () => settle(null));
6905
+ request.on("close", () => settle(null));
6906
+ request.on("response", (response) => {
6907
+ response.resume();
6908
+ response.on("end", () => settle(response.statusCode ?? null));
6909
+ response.on("error", () => settle(null));
6910
+ });
6911
+ request.end(body);
5544
6912
  });
5545
6913
  }
5546
6914
 
5547
6915
  // src/internal/capture/run-channel.ts
5548
- var net2 = __toESM(require("net"));
5549
6916
  var CONNECT_MS = 2e3;
5550
- var BAR_HIDDEN = "#__piwi-pause-bar { display: none !important; }";
6917
+ var BAR_HIDDEN2 = "#__piwi-pause-bar { display: none !important; }";
5551
6918
  var OUTLINE_STYLE = "outline: 2px dashed #a78bfa; outline-offset: 2px; background: rgba(167, 139, 250, 0.15);";
5552
6919
  var WENT_ON = "The test went on.";
6920
+ var MAX_FRAME_BACKLOG = 1024 * 1024;
6921
+ var SHOT_MS2 = 500;
6922
+ var LIGHT_QUALITY2 = 30;
6923
+ var TOO_HEAVY2 = `This page is too heavy for the live view: each image of it is over ${MAX_VIEW_FRAME / 1024} KB, a lighter one included. The image of the stop still shows it.`;
6924
+ var VIEW_END_MS2 = 1e3;
6925
+ var PAGE_ANSWER_MS = 1e4;
6926
+ var NO_ANSWER = "The page did not answer in time.";
6927
+ var LATE_ACT = "The page did not answer in time: the action may still take effect, and is told when it does.";
6928
+ var MAX_INPUT_WAITING = 4;
6929
+ var NEEDS_VIEW = "The input needs the live view of this stop: watch it first.";
6930
+ var NO_PAGE2 = "This stop has no page to show: its call does not name one among its context's pages. Use the pause bar in the test's window.";
5553
6931
  var socket = null;
5554
6932
  var control = null;
5555
6933
  var shown = /* @__PURE__ */ new Set();
6934
+ var host = null;
5556
6935
  var stops = 0;
5557
6936
  var held = null;
5558
6937
  var outline = null;
5559
6938
  var counting = Promise.resolve();
6939
+ var view2 = null;
6940
+ var viewStarting = null;
6941
+ var viewTurn = Promise.resolve();
6942
+ var viewEnds = /* @__PURE__ */ new Set();
6943
+ function freshInput() {
6944
+ return {
6945
+ rate: new RateWindow(MAX_VIEW_EVENT_RATE),
6946
+ playing: Promise.resolve(),
6947
+ waiting: 0,
6948
+ buttons: /* @__PURE__ */ new Set(),
6949
+ keys: /* @__PURE__ */ new Set()
6950
+ };
6951
+ }
6952
+ var input = freshInput();
6953
+ var pickingAt = false;
6954
+ var runName = "";
6955
+ var eventsOn = false;
6956
+ var testRunning = false;
6957
+ var eventRate = new RateWindow(MAX_RUN_EVENT_RATE);
6958
+ var droppedEvents = 0;
6959
+ var agentTurn = Promise.resolve();
6960
+ var bound = null;
6961
+ var sharedHere = false;
6962
+ var liveSaid = "";
6963
+ var unhearLive = null;
6964
+ var NO_AGENT_PAGE = "This stop has no page an agent can read: its call does not name one among its context's pages.";
6965
+ function workerTakes(env = process.env) {
6966
+ const pairing = !!env[PIWI_LOCAL_DEBUG_ENV.editorSend];
6967
+ const recorder = parseRecordChannel(env[RECORD_CHANNEL_ENV]) !== null;
6968
+ return RUN_TAKES.filter(
6969
+ (take) => take === "pick" ? pairing : take === "record" || take === "assert" ? recorder : true
6970
+ );
6971
+ }
5560
6972
  async function openRunChannel(worker, handlers, env = process.env) {
5561
6973
  const channel = parseRecordChannel(env[RUN_CHANNEL_ENV]);
5562
- const ref = env[PIWI_ORIGIN_ENV.ref];
6974
+ const ref = env[RUN_REF_ENV] ?? env[PIWI_ORIGIN_ENV.ref];
5563
6975
  if (socket || !channel || !ref || !/^[\w-]{1,64}$/.test(ref)) return false;
5564
- const opening = net2.connect(channel.path);
6976
+ const opening = net.connect(channel.path);
5565
6977
  opening.setEncoding("utf8");
5566
6978
  const opened = await new Promise((resolve5) => {
5567
6979
  const timer = setTimeout(() => resolve5(false), CONNECT_MS);
@@ -5581,6 +6993,9 @@ async function openRunChannel(worker, handlers, env = process.env) {
5581
6993
  socket = opening;
5582
6994
  control = handlers;
5583
6995
  shown = /* @__PURE__ */ new Set();
6996
+ runName = `piwi-${ref}-w${worker.workerIndex}`;
6997
+ eventsOn = false;
6998
+ host = null;
5584
6999
  opening.unref();
5585
7000
  opening.on(
5586
7001
  "data",
@@ -5592,8 +7007,13 @@ async function openRunChannel(worker, handlers, env = process.env) {
5592
7007
  opening.on("error", () => opening.destroy());
5593
7008
  opening.on("close", () => {
5594
7009
  if (socket === opening) socket = null;
7010
+ unhearLive?.();
7011
+ unhearLive = null;
7012
+ if (sharedHere) void shareLiveView(false);
7013
+ sharedHere = false;
7014
+ if (view2) void stopView2();
5595
7015
  if (!held || held.bar) return;
5596
- console.log("[piwi] The connection to the editor closed: the test goes on.");
7016
+ console.log(`[piwi] The connection to ${runHost()} closed: the test goes on.`);
5597
7017
  held.answer("resume");
5598
7018
  });
5599
7019
  tellHub({
@@ -5606,25 +7026,40 @@ async function openRunChannel(worker, handlers, env = process.env) {
5606
7026
  pid: process.pid,
5607
7027
  playwright: worker.config.version,
5608
7028
  reporter: getReporterVersion(),
5609
- takes: [...RUN_TAKES],
7029
+ takes: workerTakes(env),
5610
7030
  holds: [...RUN_HOLDS]
5611
7031
  });
7032
+ liveSaid = "";
7033
+ tellLive();
7034
+ unhearLive?.();
7035
+ unhearLive = onLiveViewChange(tellLive);
5612
7036
  return true;
5613
7037
  }
7038
+ function tellLive() {
7039
+ if (!runChannelOpen()) return;
7040
+ const state = liveViewState();
7041
+ const said = JSON.stringify(state);
7042
+ if (said === liveSaid) return;
7043
+ liveSaid = said;
7044
+ tellHub({ type: "live", ...state });
7045
+ }
5614
7046
  function editorShows(what) {
5615
7047
  return runChannelOpen() && shown.has(what);
5616
7048
  }
7049
+ function runHost() {
7050
+ return host ?? "the editor";
7051
+ }
5617
7052
  var HIDDEN_ENV = [RUN_CHANNEL_ENV, PIWI_LOCAL_DEBUG_ENV.editorSend];
5618
7053
  var hiddenEditorSend;
5619
7054
  function hideRunChannel(env = process.env) {
5620
- const hidden = HIDDEN_ENV.flatMap((name) => {
7055
+ const hidden2 = HIDDEN_ENV.flatMap((name) => {
5621
7056
  const value = env[name];
5622
7057
  return value === void 0 ? [] : [{ name, value }];
5623
7058
  });
5624
- for (const { name } of hidden) delete env[name];
5625
- hiddenEditorSend = hidden.find(({ name }) => name === PIWI_LOCAL_DEBUG_ENV.editorSend)?.value;
7059
+ for (const { name } of hidden2) delete env[name];
7060
+ hiddenEditorSend = hidden2.find(({ name }) => name === PIWI_LOCAL_DEBUG_ENV.editorSend)?.value;
5626
7061
  return () => {
5627
- for (const { name, value } of hidden) env[name] = value;
7062
+ for (const { name, value } of hidden2) env[name] = value;
5628
7063
  hiddenEditorSend = void 0;
5629
7064
  };
5630
7065
  }
@@ -5640,12 +7075,44 @@ function runChannelOpen() {
5640
7075
  return !!socket && !socket.destroyed;
5641
7076
  }
5642
7077
  function tellHub(message) {
7078
+ if (message.type === "test-begin") testRunning = true;
7079
+ if (message.type === "test-end") {
7080
+ testRunning = false;
7081
+ void unbind();
7082
+ }
5643
7083
  if (socket && !socket.destroyed) socket.write(`${JSON.stringify(message)}
5644
7084
  `);
5645
7085
  }
7086
+ function runEventsWanted() {
7087
+ return eventsOn && testRunning && runChannelOpen();
7088
+ }
7089
+ function tellEvent(event) {
7090
+ if (!runEventsWanted()) return;
7091
+ if (!eventRate.take(droppedEvents ? 2 : 1)) {
7092
+ droppedEvents++;
7093
+ return;
7094
+ }
7095
+ if (droppedEvents) tellHub({ type: "event", event: { kind: "dropped", count: droppedEvents } });
7096
+ droppedEvents = 0;
7097
+ tellHub({ type: "event", event });
7098
+ }
7099
+ async function unbind() {
7100
+ const shared = bound?.browser;
7101
+ bound = null;
7102
+ if (shared?.isConnected()) {
7103
+ await within2(
7104
+ internalCall(shared, () => bindBrowser(shared, runName, process.cwd(), false)),
7105
+ VIEW_END_MS2
7106
+ );
7107
+ }
7108
+ }
5646
7109
  function holdStop(stop, onAnswer) {
5647
7110
  const id = ++stops;
5648
7111
  let answered = false;
7112
+ const needsBar = (what) => ({
7113
+ ok: false,
7114
+ message: `${RUN_INTENT_NAMES[what]} needs the pause bar in the test's window, and this stop shows none: run the test headed.`
7115
+ });
5649
7116
  const holding = {
5650
7117
  stop: id,
5651
7118
  page: stop.page,
@@ -5655,10 +7122,13 @@ function holdStop(stop, onAnswer) {
5655
7122
  answered = true;
5656
7123
  onAnswer(choice);
5657
7124
  },
5658
- intent: (what) => answered ? { ok: false, message: WENT_ON } : stop.intent?.(what) ?? {
5659
- ok: false,
5660
- message: `${RUN_INTENT_NAMES[what]} needs the pause bar in the test's window, and this stop shows none: run the test headed.`
5661
- }
7125
+ intent: (what) => answered ? { ok: false, message: WENT_ON } : (
7126
+ // A stop no bar shows takes an intent only while its live view is watched, which drives it.
7127
+ !stop.bar && view2?.stop !== id ? needsBar(what) : stop.intent?.(what) ?? needsBar(what)
7128
+ ),
7129
+ ...stop.pickAt ? { pickAt: stop.pickAt } : {},
7130
+ ...stop.agent ? { agent: stop.agent } : {},
7131
+ browser: stop.browser ?? null
5662
7132
  };
5663
7133
  held = holding;
5664
7134
  tellHub({
@@ -5679,10 +7149,193 @@ function holdStop(stop, onAnswer) {
5679
7149
  if (held === holding) held = null;
5680
7150
  await counting;
5681
7151
  if (outline?.stop === id) await removeOutline();
7152
+ await endView(id, stop.page);
5682
7153
  tellHub({ type: "running", stop: id });
5683
7154
  }
5684
7155
  };
5685
7156
  }
7157
+ function viewEnded(stop) {
7158
+ const ended = new AbortController();
7159
+ if (view2?.stop !== stop) ended.abort();
7160
+ else viewEnds.add(() => ended.abort());
7161
+ return ended.signal;
7162
+ }
7163
+ async function within2(run, ms) {
7164
+ await answerWithin(run, ms, void 0);
7165
+ }
7166
+ async function answerWithin(run, ms, late) {
7167
+ let timer;
7168
+ try {
7169
+ return await Promise.race([
7170
+ run.catch(() => late),
7171
+ new Promise((resolve5) => timer = setTimeout(() => resolve5(late), ms))
7172
+ ]);
7173
+ } finally {
7174
+ clearTimeout(timer);
7175
+ }
7176
+ }
7177
+ async function endView(stop, page) {
7178
+ if (viewStarting?.stop === stop) await within2(viewStarting.started, VIEW_END_MS2);
7179
+ if (view2?.stop === stop) await stopView2();
7180
+ const own = input;
7181
+ input = freshInput();
7182
+ await within2(own.playing, VIEW_END_MS2);
7183
+ if (page && !page.isClosed() && (own.buttons.size || own.keys.size)) {
7184
+ await within2(
7185
+ internalCall(page, async () => {
7186
+ for (const button of own.buttons) await page.mouse.up({ button });
7187
+ for (const key of own.keys) await page.keyboard.up(key);
7188
+ }),
7189
+ VIEW_END_MS2
7190
+ );
7191
+ }
7192
+ }
7193
+ async function stopView2() {
7194
+ const shown2 = view2;
7195
+ view2 = null;
7196
+ for (const end of viewEnds) end();
7197
+ viewEnds.clear();
7198
+ if (shown2) await within2(shown2.close(), VIEW_END_MS2);
7199
+ }
7200
+ function startView2(holding) {
7201
+ if (view2?.stop === holding.stop) {
7202
+ view2.resend();
7203
+ return Promise.resolve({ ok: true, message: "The live view is on." });
7204
+ }
7205
+ if (viewStarting?.stop === holding.stop) return viewStarting.started;
7206
+ const page = holding.page;
7207
+ if (!page || page.isClosed()) return Promise.resolve({ ok: false, message: NO_PAGE2 });
7208
+ const started = (async () => {
7209
+ if (view2) await stopView2();
7210
+ const opening = openView(page, holding.stop);
7211
+ const opened = await answerWithin(opening, PAGE_ANSWER_MS, "late");
7212
+ if (opened === "late") {
7213
+ void opening.then((late) => late?.close());
7214
+ return { ok: false, message: NO_ANSWER };
7215
+ }
7216
+ if (!opened) return { ok: false, message: "The page could not be shown: it closed, or does not answer." };
7217
+ if (held !== holding || page.isClosed()) {
7218
+ await within2(opened.close(), VIEW_END_MS2);
7219
+ return { ok: false, message: WENT_ON };
7220
+ }
7221
+ view2 = opened;
7222
+ return {
7223
+ ok: true,
7224
+ message: opened.screencast ? "The live view is on." : "The live view is on, as images of the page twice a second: the page's screencast is in use."
7225
+ };
7226
+ })().finally(() => {
7227
+ if (viewStarting?.started === started) viewStarting = null;
7228
+ });
7229
+ viewStarting = { stop: holding.stop, started };
7230
+ return started;
7231
+ }
7232
+ async function openView(page, stop) {
7233
+ const opened = await openPageView(page, {
7234
+ size: viewSize,
7235
+ quality: VIEW_QUALITY,
7236
+ fps: MAX_VIEW_FPS,
7237
+ maxFrameBytes: MAX_VIEW_FRAME,
7238
+ lightQuality: LIGHT_QUALITY2,
7239
+ shotMs: SHOT_MS2,
7240
+ busy: () => !!socket && socket.writableLength > MAX_FRAME_BACKLOG,
7241
+ // Once its stop began to end, no frame of it goes.
7242
+ live: () => held?.stop === stop,
7243
+ send: (frame) => tellHub({ type: "frame", stop, width: frame.width, height: frame.height, data: frame.data.toString("base64") }),
7244
+ refuse: () => tellHub({ type: "frame", stop, refused: TOO_HEAVY2 })
7245
+ });
7246
+ if (!opened) return null;
7247
+ const onClose = () => {
7248
+ if (view2?.stop === stop) void stopView2();
7249
+ };
7250
+ page.once("close", onClose);
7251
+ return {
7252
+ stop,
7253
+ screencast: opened.screencast,
7254
+ resend: opened.resend,
7255
+ close: async () => {
7256
+ page.off("close", onClose);
7257
+ await opened.close();
7258
+ }
7259
+ };
7260
+ }
7261
+ async function stopViewOf(holding) {
7262
+ if (viewStarting?.stop === holding.stop) await within2(viewStarting.started, VIEW_END_MS2);
7263
+ if (view2?.stop === holding.stop) await stopView2();
7264
+ return { ok: true, message: "The live view is off." };
7265
+ }
7266
+ function playInput(holding, events) {
7267
+ const page = holding.page;
7268
+ if (!page || view2?.stop !== holding.stop) return Promise.resolve({ ok: false, message: NEEDS_VIEW });
7269
+ const own = input;
7270
+ if (own.waiting >= MAX_INPUT_WAITING) {
7271
+ return Promise.resolve({ ok: false, message: "The page still plays the input sent before: send less at once." });
7272
+ }
7273
+ if (!own.rate.take(events.length)) {
7274
+ return Promise.resolve({ ok: false, message: `Too much input: ${MAX_VIEW_EVENT_RATE} events a second at most.` });
7275
+ }
7276
+ own.waiting++;
7277
+ const played = own.playing.then(async () => {
7278
+ for (const [i, event] of events.entries()) {
7279
+ if (held !== holding || input !== own || view2?.stop !== holding.stop || page.isClosed()) {
7280
+ return { ok: false, message: i ? `The stop ended after ${i} of the events.` : WENT_ON };
7281
+ }
7282
+ try {
7283
+ await internalCall(page, () => playEvent(page, event, own));
7284
+ } catch (error) {
7285
+ return {
7286
+ ok: false,
7287
+ message: hideSecretsInText(`Event ${i + 1} could not be played: ${errorMessage(error)}`)
7288
+ };
7289
+ }
7290
+ }
7291
+ return { ok: true, message: events.length === 1 ? "Played 1 event." : `Played ${events.length} events.` };
7292
+ }).finally(() => own.waiting--);
7293
+ own.playing = played.catch(() => void 0);
7294
+ return played;
7295
+ }
7296
+ async function playEvent(page, event, own) {
7297
+ switch (event.type) {
7298
+ case "mouse":
7299
+ if (event.action === "move") return page.mouse.move(event.x, event.y);
7300
+ if (event.action === "click") {
7301
+ return page.mouse.click(event.x, event.y, { button: event.button, clickCount: event.clickCount });
7302
+ }
7303
+ await page.mouse.move(event.x, event.y);
7304
+ const pressed = {
7305
+ button: event.button,
7306
+ ...event.clickCount === void 0 ? {} : { clickCount: event.clickCount }
7307
+ };
7308
+ if (event.action === "down") {
7309
+ own.buttons.add(event.button);
7310
+ return page.mouse.down(pressed);
7311
+ }
7312
+ own.buttons.delete(event.button);
7313
+ return page.mouse.up(pressed);
7314
+ case "wheel":
7315
+ await page.mouse.move(event.x, event.y);
7316
+ return page.mouse.wheel(event.deltaX, event.deltaY);
7317
+ case "key":
7318
+ if (event.action === "press") return page.keyboard.press(event.key);
7319
+ if (event.action === "down") {
7320
+ own.keys.add(event.key);
7321
+ return page.keyboard.down(event.key);
7322
+ }
7323
+ own.keys.delete(event.key);
7324
+ return page.keyboard.up(event.key);
7325
+ case "text":
7326
+ return page.keyboard.insertText(event.text);
7327
+ }
7328
+ }
7329
+ async function pickAtStop(holding, x, y) {
7330
+ if (!holding.pickAt) return { ok: false, message: NO_PAGE2, locator: null };
7331
+ if (pickingAt) return { ok: false, message: "A pick on the image runs: wait for its answer.", locator: null };
7332
+ pickingAt = true;
7333
+ try {
7334
+ return await answerWithin(holding.pickAt(x, y), PAGE_ANSWER_MS, { ok: false, message: NO_ANSWER, locator: null });
7335
+ } finally {
7336
+ pickingAt = false;
7337
+ }
7338
+ }
5686
7339
  async function removeOutline() {
5687
7340
  const shown2 = outline;
5688
7341
  outline = null;
@@ -5714,78 +7367,514 @@ async function countLocator(holding, source) {
5714
7367
  return { ok: false, message: `The locator could not be counted: ${errorMessage(error)}`, count: null };
5715
7368
  }
5716
7369
  }
5717
- function onMessage(message) {
5718
- if (!message) return;
5719
- switch (message.type) {
5720
- case "attached":
5721
- shown = new Set(message.holds);
5722
- return;
5723
- case "breakpoints":
5724
- control?.breakpoints(message.places);
5725
- return;
5726
- case "control":
5727
- if (message.action === "pause") {
5728
- if (!held) control?.pause();
5729
- } else if (held?.stop === message.stop) held.answer(message.action);
5730
- return;
5731
- case "run-to":
5732
- if (held?.stop !== message.stop) return;
5733
- control?.runTo(message.place);
5734
- held.answer("resume");
5735
- return;
5736
- case "request": {
5737
- const { request } = message;
5738
- const at = message.what !== "image" && held?.stop === message.stop ? held : null;
5739
- switch (message.what) {
5740
- case "image":
5741
- void imageOf(held?.page ?? null).then((value) => tellHub({ type: "answer", request, what: "image", value }));
5742
- return;
5743
- case "requests": {
5744
- const requests = at ? keptRequests() : [];
5745
- const said = !at ? WENT_ON : requests.length ? `The test kept ${requests.length === 1 ? "1 response" : `${requests.length} responses`}.` : "The test kept no response yet: Piwi keeps the text answers of its fetch and XHR requests.";
5746
- tellHub({ type: "answer", request, what: "requests", value: { ok: !!at, message: said, requests } });
5747
- return;
5748
- }
5749
- case "mock": {
5750
- void (at ? keptResponse(message.id) : Promise.resolve({ refusal: WENT_ON })).then((found) => {
5751
- const value = "refusal" in found ? { ok: false, message: found.refusal, response: null } : { ok: true, message: `${found.method} ${found.url}`, response: found };
5752
- tellHub({ type: "answer", request, what: "mock", value });
5753
- });
5754
- return;
5755
- }
5756
- case "locator": {
5757
- const counted = at ? counting.then(
5758
- () => held === at ? countLocator(at, message.locator) : { ok: false, message: WENT_ON, count: null }
5759
- ) : Promise.resolve({ ok: false, message: WENT_ON, count: null });
5760
- counting = counted.catch(() => void 0);
5761
- void counted.then((value) => tellHub({ type: "answer", request, what: "locator", value }));
5762
- return;
5763
- }
5764
- default: {
5765
- const value = at ? at.intent(message.what) : { ok: false, message: WENT_ON };
5766
- tellHub({ type: "answer", request, what: message.what, value });
5767
- return;
7370
+ function onMessage(message) {
7371
+ if (!message) return;
7372
+ switch (message.type) {
7373
+ case "attached":
7374
+ shown = new Set(message.holds);
7375
+ host = message.host ?? null;
7376
+ return;
7377
+ case "events":
7378
+ eventsOn = message.on;
7379
+ eventRate = new RateWindow(MAX_RUN_EVENT_RATE);
7380
+ droppedEvents = 0;
7381
+ return;
7382
+ case "share":
7383
+ sharedHere = message.on;
7384
+ void shareLiveView(message.on).then(tellLive);
7385
+ return;
7386
+ case "breakpoints":
7387
+ control?.breakpoints(message.places);
7388
+ return;
7389
+ case "control":
7390
+ if (message.action === "pause") {
7391
+ if (!held) control?.pause();
7392
+ } else if (held?.stop === message.stop) held.answer(message.action);
7393
+ return;
7394
+ case "run-to":
7395
+ if (held?.stop !== message.stop) return;
7396
+ control?.runTo(message.place);
7397
+ held.answer("resume");
7398
+ return;
7399
+ case "request": {
7400
+ const { request } = message;
7401
+ const at = message.what !== "image" && held?.stop === message.stop ? held : null;
7402
+ switch (message.what) {
7403
+ case "image":
7404
+ void imageOf(held?.page ?? null).then((value) => tellHub({ type: "answer", request, what: "image", value }));
7405
+ return;
7406
+ case "requests": {
7407
+ const requests = at ? keptRequests() : [];
7408
+ const said = !at ? WENT_ON : requests.length ? `The test kept ${requests.length === 1 ? "1 response" : `${requests.length} responses`}.` : "The test kept no response yet: Piwi keeps the text answers of its fetch and XHR requests.";
7409
+ tellHub({ type: "answer", request, what: "requests", value: { ok: !!at, message: said, requests } });
7410
+ return;
7411
+ }
7412
+ case "mock": {
7413
+ void (at ? keptResponse(message.id) : Promise.resolve({ refusal: WENT_ON })).then((found) => {
7414
+ const value = "refusal" in found ? { ok: false, message: found.refusal, response: null } : { ok: true, message: `${found.method} ${found.url}`, response: found };
7415
+ tellHub({ type: "answer", request, what: "mock", value });
7416
+ });
7417
+ return;
7418
+ }
7419
+ case "locator": {
7420
+ const counted = at ? counting.then(
7421
+ () => held === at ? countLocator(at, message.locator) : { ok: false, message: WENT_ON, count: null }
7422
+ ) : Promise.resolve({ ok: false, message: WENT_ON, count: null });
7423
+ counting = counted.catch(() => void 0);
7424
+ void counted.then((value) => tellHub({ type: "answer", request, what: "locator", value }));
7425
+ return;
7426
+ }
7427
+ case "view": {
7428
+ const on = message.on;
7429
+ const answered = viewTurn.then(
7430
+ () => !at ? Promise.resolve({ ok: false, message: WENT_ON }) : on ? startView2(at) : stopViewOf(at)
7431
+ );
7432
+ viewTurn = answered.catch(() => void 0);
7433
+ void answered.then((value) => tellHub({ type: "answer", request, what: "view", value }));
7434
+ return;
7435
+ }
7436
+ case "input": {
7437
+ const played = at ? playInput(at, message.events) : Promise.resolve({ ok: false, message: WENT_ON });
7438
+ void played.then((value) => tellHub({ type: "answer", request, what: "input", value }));
7439
+ return;
7440
+ }
7441
+ case "pick-at": {
7442
+ const picked = at ? pickAtStop(at, message.x, message.y) : Promise.resolve({ ok: false, message: WENT_ON, locator: null });
7443
+ void picked.then((value) => tellHub({ type: "answer", request, what: "pick-at", value }));
7444
+ return;
7445
+ }
7446
+ case "snapshot": {
7447
+ const { depth, image } = message;
7448
+ const none = { url: "", title: "", snapshot: null, cut: false, image: null };
7449
+ void agentAt(
7450
+ at,
7451
+ (agent) => agent.snapshot(depth, image),
7452
+ (refused) => ({ ...refused, ...none })
7453
+ ).then((value) => tellHub({ type: "answer", request, what: "snapshot", value }));
7454
+ return;
7455
+ }
7456
+ case "ref-locator": {
7457
+ const { ref } = message;
7458
+ void agentAt(
7459
+ at,
7460
+ (agent) => agent.refLocator(ref),
7461
+ (refused) => ({ ...refused, element: null, locators: [] })
7462
+ ).then((value) => tellHub({ type: "answer", request, what: "ref-locator", value }));
7463
+ return;
7464
+ }
7465
+ case "act": {
7466
+ const { ref, action, value: given } = message;
7467
+ const answer = (value) => tellHub({ type: "answer", request, what: "act", value });
7468
+ void agentAt(
7469
+ at,
7470
+ (agent) => agent.act({ ref, action, value: given }),
7471
+ (refused) => ({ ...refused, element: null, locators: [] }),
7472
+ { answer: { ok: false, message: LATE_ACT, element: null, locators: [], late: true }, again: answer }
7473
+ ).then(answer);
7474
+ return;
7475
+ }
7476
+ case "bind": {
7477
+ void bindAt(at, message.on).then((value) => tellHub({ type: "answer", request, what: "bind", value }));
7478
+ return;
7479
+ }
7480
+ default: {
7481
+ const value = at ? at.intent(message.what) : { ok: false, message: WENT_ON };
7482
+ tellHub({ type: "answer", request, what: message.what, value });
7483
+ return;
7484
+ }
7485
+ }
7486
+ }
7487
+ case "unknown":
7488
+ tellHub({
7489
+ type: "notice",
7490
+ message: `@piwitests/reporter ${getReporterVersion()} does not take the editor's ${message.what}: update it.`
7491
+ });
7492
+ }
7493
+ }
7494
+ function agentAt(at, run, refuse2, late) {
7495
+ if (!at) return Promise.resolve(refuse2({ ok: false, message: WENT_ON }));
7496
+ const agent = at.agent;
7497
+ if (!agent) return Promise.resolve(refuse2({ ok: false, message: NO_AGENT_PAGE }));
7498
+ const answered = agentTurn.then(async () => {
7499
+ if (held !== at) return refuse2({ ok: false, message: WENT_ON });
7500
+ const running2 = run(agent);
7501
+ const settled = running2.then(
7502
+ (value) => ({ value }),
7503
+ () => ({ value: refuse2({ ok: false, message: NO_ANSWER }) })
7504
+ );
7505
+ const said = await answerWithin(settled, PAGE_ANSWER_MS, null);
7506
+ if (said) return said.value;
7507
+ if (!late) return refuse2({ ok: false, message: NO_ANSWER });
7508
+ void running2.then((value) => value.ok && held === at && late.again(value)).catch(() => void 0);
7509
+ return late.answer;
7510
+ });
7511
+ agentTurn = answered.catch(() => void 0);
7512
+ return answered;
7513
+ }
7514
+ async function bindAt(at, on) {
7515
+ if (!at) return { ok: false, message: WENT_ON, name: null, endpoint: null };
7516
+ const browser = at.browser;
7517
+ if (!on) {
7518
+ await unbind();
7519
+ return { ok: true, message: "The browser is no longer shared with Playwright tools.", name: null, endpoint: null };
7520
+ }
7521
+ if (!browser) return { ok: false, message: NO_AGENT_PAGE, name: null, endpoint: null };
7522
+ if (bound?.browser === browser) {
7523
+ return {
7524
+ ok: true,
7525
+ message: `The browser is shared with Playwright tools as ${runName} until the test ends: any process of yours can drive it.`,
7526
+ name: runName,
7527
+ endpoint: bound.endpoint
7528
+ };
7529
+ }
7530
+ if (bound) await unbind();
7531
+ const answer = await internalCall(browser, () => bindBrowser(browser, runName, process.cwd(), true));
7532
+ if (answer.ok) bound = { browser, endpoint: answer.endpoint };
7533
+ return answer;
7534
+ }
7535
+ async function imageOf(page) {
7536
+ if (!page || page.isClosed()) return null;
7537
+ try {
7538
+ const shot = await internalCall(
7539
+ page,
7540
+ () => page.screenshot({ type: "jpeg", quality: 60, style: BAR_HIDDEN2, timeout: 5e3 })
7541
+ );
7542
+ const viewport = await viewportOf(page);
7543
+ return { mime: "image/jpeg", data: shot.toString("base64"), ...viewport };
7544
+ } catch {
7545
+ return null;
7546
+ }
7547
+ }
7548
+
7549
+ // src/internal/capture/run-events.ts
7550
+ var begun = /* @__PURE__ */ new WeakMap();
7551
+ var heard = /* @__PURE__ */ new WeakSet();
7552
+ var watched = /* @__PURE__ */ new WeakSet();
7553
+ function userFile(file, cwd, windows) {
7554
+ if (!file || /[\\/]node_modules[\\/]/.test(file)) return null;
7555
+ const relative6 = callSitePath(file, cwd, windows);
7556
+ if (!relative6 || (windows ? path9.win32 : path9.posix).isAbsolute(relative6) || relative6.startsWith("..")) return null;
7557
+ return relative6;
7558
+ }
7559
+ function callPlace(frames, cwd = process.cwd(), windows = process.platform === "win32") {
7560
+ for (const frame of frames ?? []) {
7561
+ const file = userFile(frame.file, cwd, windows);
7562
+ if (!file || !frame.line || frame.line < 1) continue;
7563
+ return { file, line: frame.line, ...frame.column && frame.column > 0 ? { column: frame.column } : {} };
7564
+ }
7565
+ return null;
7566
+ }
7567
+ function callTitle(zone, channel) {
7568
+ const selector = typeof channel?.params?.selector === "string" ? ` ${channel.params.selector}` : "";
7569
+ const title = zone.title || `${zone.apiName || "call"}${selector}`;
7570
+ return hideSecretsInText(title).slice(0, 300);
7571
+ }
7572
+ function errorText(error) {
7573
+ const message = error instanceof Error ? error.message : String(error);
7574
+ return hideSecretsInText(message.split("\n").slice(0, 4).join("\n")).slice(0, MAX_RUN_EVENT_TEXT);
7575
+ }
7576
+ function watchCalls(playwright) {
7577
+ const instrumentation = playwright?._instrumentation;
7578
+ if (!playwright || heard.has(playwright)) return true;
7579
+ if (!instrumentation || typeof instrumentation.addListener !== "function") return false;
7580
+ heard.add(playwright);
7581
+ const titles = /* @__PURE__ */ new WeakMap();
7582
+ instrumentation.addListener({
7583
+ onApiCallBegin: (zone, channel) => {
7584
+ if (!runEventsWanted()) return;
7585
+ begun.set(zone, Date.now());
7586
+ titles.set(zone, callTitle(zone, channel));
7587
+ },
7588
+ onApiCallEnd: (zone) => {
7589
+ const start = begun.get(zone);
7590
+ if (start === void 0) return;
7591
+ begun.delete(zone);
7592
+ if (!runEventsWanted()) return;
7593
+ tellEvent({
7594
+ kind: "call",
7595
+ title: titles.get(zone) ?? callTitle(zone, void 0),
7596
+ place: callPlace(zone.frames),
7597
+ ok: zone.error === void 0,
7598
+ error: zone.error === void 0 ? null : errorText(zone.error),
7599
+ ms: Date.now() - start
7600
+ });
7601
+ }
7602
+ });
7603
+ return true;
7604
+ }
7605
+ function watchPageEvents(page) {
7606
+ if (watched.has(page) || typeof page.on !== "function") return;
7607
+ watched.add(page);
7608
+ const text = (value) => hideSecretsInText(value).slice(0, MAX_RUN_EVENT_TEXT);
7609
+ const url = (value) => hideSecretsInUrl(value).slice(0, 8192);
7610
+ const guard = (tell) => {
7611
+ if (!runEventsWanted()) return;
7612
+ try {
7613
+ tell();
7614
+ } catch {
7615
+ }
7616
+ };
7617
+ page.on(
7618
+ "framenavigated",
7619
+ (frame) => guard(() => {
7620
+ if (frame === page.mainFrame()) tellEvent({ kind: "navigation", url: url(frame.url()) });
7621
+ })
7622
+ );
7623
+ page.on(
7624
+ "console",
7625
+ (message) => guard(() => {
7626
+ const type = message.type();
7627
+ if (type === "error" || type === "warning")
7628
+ tellEvent({ kind: "console", level: type, text: text(message.text()) });
7629
+ })
7630
+ );
7631
+ page.on("pageerror", (error) => guard(() => tellEvent({ kind: "page-error", text: text(errorText(error)) })));
7632
+ page.on(
7633
+ "requestfailed",
7634
+ (request) => guard(
7635
+ () => tellEvent({
7636
+ kind: "request-failed",
7637
+ method: request.method(),
7638
+ url: url(request.url()),
7639
+ status: null,
7640
+ failure: request.failure()?.errorText ? text(request.failure().errorText).slice(0, 500) : null
7641
+ })
7642
+ )
7643
+ );
7644
+ page.on(
7645
+ "response",
7646
+ (response) => guard(() => {
7647
+ const status = response.status();
7648
+ if (status < 400 || status > 599) return;
7649
+ const request = response.request();
7650
+ tellEvent({ kind: "request-failed", method: request.method(), url: url(response.url()), status, failure: null });
7651
+ })
7652
+ );
7653
+ }
7654
+
7655
+ // src/internal/capture/region-snapshot.ts
7656
+ function regionSnapshotWithoutSecrets(snapshot, secrets) {
7657
+ const hidden2 = new Set(secrets.filter(Boolean));
7658
+ const kept = [];
7659
+ for (const line of snapshot.split("\n")) {
7660
+ const box = /^(\s*- textbox(?: "(?:[^"\\]|\\.)*")?(?: \[[^\]]*\])*): (.*)$/.exec(line);
7661
+ if (box && hidden2.size && hidden2.has(yamlScalar(box[2]))) {
7662
+ kept.push({ line: box[1], lostChild: false });
7663
+ continue;
7664
+ }
7665
+ const url = /^\s*- \/url: (.*)$/.exec(line);
7666
+ if (url && hideSecretsInUrl(yamlScalar(url[1])) !== yamlScalar(url[1])) {
7667
+ let parent = kept.length - 1;
7668
+ while (parent >= 0 && indentOf(kept[parent].line) >= indentOf(line)) parent--;
7669
+ if (parent >= 0) kept[parent].lostChild = true;
7670
+ continue;
7671
+ }
7672
+ kept.push({ line, lostChild: false });
7673
+ }
7674
+ return kept.map(({ line, lostChild }, i) => {
7675
+ const next = kept[i + 1];
7676
+ const childless = !next || indentOf(next.line) <= indentOf(line);
7677
+ return lostChild && childless && line.endsWith(":") ? line.slice(0, -1) : line;
7678
+ }).join("\n");
7679
+ }
7680
+ function indentOf(line) {
7681
+ return line.length - line.trimStart().length;
7682
+ }
7683
+ function yamlScalar(text) {
7684
+ const value = text.trim();
7685
+ if (value.startsWith('"')) {
7686
+ try {
7687
+ return String(JSON.parse(value));
7688
+ } catch {
7689
+ return value;
7690
+ }
7691
+ }
7692
+ if (value.startsWith("'") && value.endsWith("'") && value.length > 1) return value.slice(1, -1).replace(/''/g, "'");
7693
+ return value;
7694
+ }
7695
+
7696
+ // src/internal/capture/record-handoff.ts
7697
+ var fs6 = __toESM(require("fs"));
7698
+ var net2 = __toESM(require("net"));
7699
+ var path10 = __toESM(require("path"));
7700
+ var START_WAIT_MS = 6e4;
7701
+ var NEW_TAB_NOTICE = "A new tab opened: what you do there is not recorded yet. Record in the first tab.";
7702
+ var relays = /* @__PURE__ */ new WeakMap();
7703
+ function idleAnswer(payload) {
7704
+ const request = payload ?? {};
7705
+ if (request.kind === "local-get") return {};
7706
+ if (request.kind !== "message") return null;
7707
+ if (request.message?.type === "piwi-ping") return { ok: true };
7708
+ if (request.message?.type === "piwi-session-storage") {
7709
+ return request.message.op === "get" ? { ok: true, items: {} } : { ok: true };
7710
+ }
7711
+ return { ok: false, error: "No recording runs in this page." };
7712
+ }
7713
+ function canRecordFromHere(channel = process.env[RECORD_CHANNEL_ENV]) {
7714
+ return parseRecordChannel(channel) !== null;
7715
+ }
7716
+ function handoffPlace(location, cwd = process.cwd()) {
7717
+ const m = /^(.+):(\d+):\d+$/.exec(location);
7718
+ if (!m) return null;
7719
+ return { file: path10.resolve(cwd, m[1]), line: Number(m[2]) };
7720
+ }
7721
+ async function recordFromHere(options) {
7722
+ const channel = parseRecordChannel(options.channel ?? process.env[RECORD_CHANNEL_ENV]);
7723
+ const place = handoffPlace(options.location);
7724
+ if (!channel || !place) return "this run names no editor to record into.";
7725
+ const { page, testInfo, ended } = options;
7726
+ if (ended?.aborted) return "the test ended.";
7727
+ const use = testInfo.project?.use ?? {};
7728
+ const context = page.context();
7729
+ return new Promise((resolve5) => {
7730
+ const socket2 = net2.connect(channel.path);
7731
+ socket2.setEncoding("utf8");
7732
+ const answers = /* @__PURE__ */ new Map();
7733
+ let calls = 0;
7734
+ let attached = false;
7735
+ let recording = false;
7736
+ let settled = false;
7737
+ let initScript;
7738
+ const send2 = (message) => {
7739
+ if (!socket2.destroyed) socket2.write(`${JSON.stringify(message)}
7740
+ `);
7741
+ };
7742
+ const onPage = () => send2({ type: "notice", message: NEW_TAB_NOTICE });
7743
+ const onGone = () => {
7744
+ if (attached) send2({ type: "closed" });
7745
+ finish("the page closed.");
7746
+ };
7747
+ const onEnded = () => {
7748
+ try {
7749
+ if (recording) {
7750
+ void internalCall(
7751
+ page,
7752
+ () => page.evaluate(
7753
+ ([name, value]) => {
7754
+ const dispatch = globalThis[name];
7755
+ if (typeof dispatch === "function") dispatch(value);
7756
+ },
7757
+ [IDE_DISPATCH_GLOBAL, { type: "piwi-recording-stopped" }]
7758
+ )
7759
+ ).catch(() => void 0);
5768
7760
  }
7761
+ if (attached) send2({ type: "closed" });
7762
+ finish("the test ended.");
7763
+ } catch {
5769
7764
  }
5770
- }
5771
- case "unknown":
5772
- tellHub({
5773
- type: "notice",
5774
- message: `@piwitests/reporter ${getReporterVersion()} does not take the editor's ${message.what}: update it.`
7765
+ };
7766
+ page.once("close", onGone);
7767
+ page.once("crash", onGone);
7768
+ ended?.addEventListener("abort", onEnded, { once: true });
7769
+ const startWait = setTimeout(() => finish("the editor did not start the recording."), START_WAIT_MS);
7770
+ const release = () => {
7771
+ const relay = relays.get(context);
7772
+ if (relay && relay.recording === relayed) relay.recording = null;
7773
+ void Promise.resolve(initScript?.dispose?.()).catch(() => void 0);
7774
+ initScript = void 0;
7775
+ };
7776
+ const finish = (said) => {
7777
+ if (settled) return;
7778
+ settled = true;
7779
+ clearTimeout(startWait);
7780
+ release();
7781
+ for (const [call, answer] of answers) {
7782
+ answers.delete(call);
7783
+ answer(null);
7784
+ }
7785
+ context.off("page", onPage);
7786
+ page.off("close", onGone);
7787
+ page.off("crash", onGone);
7788
+ ended?.removeEventListener("abort", onEnded);
7789
+ socket2.end();
7790
+ resolve5(recording ? null : said);
7791
+ };
7792
+ const relayed = (source, payload) => new Promise((answer) => {
7793
+ const call = ++calls;
7794
+ answers.set(call, answer);
7795
+ const outside = source.page !== page || source.frame !== page.mainFrame();
7796
+ send2({ type: "binding", call, outside, payload });
7797
+ });
7798
+ const load = async (bundle) => {
7799
+ if (!/^https?:/i.test(page.url())) {
7800
+ const why = `the recorder records http(s) pages only, and this page is ${page.url() || "blank"}.`;
7801
+ send2({ type: "notice", message: `Nothing can be recorded here: ${why}` });
7802
+ finish(why);
7803
+ return;
7804
+ }
7805
+ let relay = relays.get(context);
7806
+ if (!relay) {
7807
+ const created = { recording: null, lingering: false };
7808
+ await context.exposeBinding(
7809
+ IDE_RECORDER_BINDING,
7810
+ (source, payload) => created.recording ? created.recording(source, payload) : idleAnswer(payload)
7811
+ );
7812
+ relays.set(context, created);
7813
+ relay = created;
7814
+ }
7815
+ if (settled) return;
7816
+ relay.recording = relayed;
7817
+ if (!relay.lingering) {
7818
+ initScript = await context.addInitScript({ path: bundle });
7819
+ if (typeof initScript?.dispose !== "function") relay.lingering = true;
7820
+ }
7821
+ if (settled) return release();
7822
+ await page.evaluate(fs6.readFileSync(bundle, "utf-8"));
7823
+ if (settled) return release();
7824
+ recording = true;
7825
+ clearTimeout(startWait);
7826
+ context.on("page", onPage);
7827
+ send2({ type: "loaded" });
7828
+ };
7829
+ socket2.on("connect", () => {
7830
+ attached = true;
7831
+ send2({
7832
+ type: "attach",
7833
+ token: channel.token,
7834
+ file: place.file,
7835
+ line: place.line,
7836
+ reason: options.reason,
7837
+ testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
7838
+ baseURL: typeof use.baseURL === "string" ? use.baseURL : null,
7839
+ ...options.mode === "check" ? { mode: "check", stop: options.stop ?? null } : {}
5775
7840
  });
5776
- }
5777
- }
5778
- async function imageOf(page) {
5779
- if (!page || page.isClosed()) return null;
5780
- try {
5781
- const shot = await internalCall(
5782
- page,
5783
- () => page.screenshot({ type: "jpeg", quality: 60, style: BAR_HIDDEN, timeout: 5e3 })
7841
+ });
7842
+ socket2.on(
7843
+ "data",
7844
+ lineReader(
7845
+ (line) => {
7846
+ const message = parseHandoffToTest(line);
7847
+ if (!message || settled) return;
7848
+ switch (message.type) {
7849
+ case "refused":
7850
+ return finish(message.message);
7851
+ case "load":
7852
+ return void load(message.bundle).catch(
7853
+ (error) => finish(`the recorder could not be loaded: ${error instanceof Error ? error.message : String(error)}`)
7854
+ );
7855
+ case "answer": {
7856
+ const answer = answers.get(message.call);
7857
+ answers.delete(message.call);
7858
+ return answer?.(message.value);
7859
+ }
7860
+ case "dispatch":
7861
+ return void page.evaluate(
7862
+ ([name, value]) => {
7863
+ const dispatch = globalThis[name];
7864
+ if (typeof dispatch === "function") dispatch(value);
7865
+ },
7866
+ [IDE_DISPATCH_GLOBAL, message.message]
7867
+ ).catch(() => void 0);
7868
+ case "stop":
7869
+ return finish(null);
7870
+ }
7871
+ },
7872
+ () => finish("the editor sent a message too long to read.")
7873
+ )
5784
7874
  );
5785
- return { mime: "image/jpeg", data: shot.toString("base64") };
5786
- } catch {
5787
- return null;
5788
- }
7875
+ socket2.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
7876
+ socket2.on("close", () => finish("the editor closed the recording."));
7877
+ });
5789
7878
  }
5790
7879
 
5791
7880
  // src/internal/collect/coverage-settings.ts
@@ -5806,7 +7895,7 @@ function browserFunctionsInWorker() {
5806
7895
  function browserFunctionCoverage(files) {
5807
7896
  return sanitizeCoverage({
5808
7897
  v: 1,
5809
- files: [...files].map(([path15, functions]) => ({ path: path15, runtime: "browser", functions }))
7898
+ files: [...files].map(([path16, functions]) => ({ path: path16, runtime: "browser", functions }))
5810
7899
  });
5811
7900
  }
5812
7901
  function newKnownFunctions(known, sent2) {
@@ -5819,19 +7908,19 @@ function newKnownFunctions(known, sent2) {
5819
7908
  budget = MAX_COVERAGE_RANGES;
5820
7909
  };
5821
7910
  for (const [raw, ranges] of known) {
5822
- const path15 = normalizeCoveragePath(raw);
5823
- if (path15 === null) continue;
5824
- let named = sent2.get(path15);
7911
+ const path16 = normalizeCoveragePath(raw);
7912
+ if (path16 === null) continue;
7913
+ let named = sent2.get(path16);
5825
7914
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
5826
7915
  if (fresh.length === 0) continue;
5827
- if (!named) sent2.set(path15, named = /* @__PURE__ */ new Set());
7916
+ if (!named) sent2.set(path16, named = /* @__PURE__ */ new Set());
5828
7917
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
5829
7918
  while (fresh.length > 0) {
5830
7919
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
5831
7920
  const part = fresh.slice(0, budget);
5832
7921
  fresh = fresh.slice(part.length);
5833
7922
  budget -= part.length;
5834
- files.push({ path: path15, allFunctions: part });
7923
+ files.push({ path: path16, allFunctions: part });
5835
7924
  }
5836
7925
  }
5837
7926
  close();
@@ -5842,7 +7931,7 @@ function serverCoverageInWorker() {
5842
7931
  }
5843
7932
 
5844
7933
  // src/internal/support/aria-sampling.ts
5845
- var path10 = __toESM(require("path"));
7934
+ var path11 = __toESM(require("path"));
5846
7935
  var os3 = __toESM(require("os"));
5847
7936
  var fs7 = __toESM(require("fs"));
5848
7937
 
@@ -5858,7 +7947,7 @@ function ariaSampleIdentity(filePath2, title) {
5858
7947
  return `${filePath2}\0${title}`;
5859
7948
  }
5860
7949
  function getAriaSampleFilePath(projectName) {
5861
- return path10.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
7950
+ return path11.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
5862
7951
  }
5863
7952
  var cachedSets = /* @__PURE__ */ new Map();
5864
7953
  function loadAriaSampleSet(projectName) {
@@ -5889,7 +7978,7 @@ function isDueForAriaSample(testInfo) {
5889
7978
  // src/internal/capture/code-reach.ts
5890
7979
  var crypto3 = __toESM(require("crypto"));
5891
7980
  var fs8 = __toESM(require("fs"));
5892
- var path11 = __toESM(require("path"));
7981
+ var path12 = __toESM(require("path"));
5893
7982
  var import_node_child_process = require("child_process");
5894
7983
  var MAX_MAP_BYTES = 20 * 1024 * 1024;
5895
7984
  var MAP_TIMEOUT_MS = 5e3;
@@ -5932,12 +8021,12 @@ function repositoryRoot(from) {
5932
8021
  }).trim();
5933
8022
  } catch {
5934
8023
  }
5935
- cachedRepoRoot = { from, root: path11.resolve(root) };
8024
+ cachedRepoRoot = { from, root: path12.resolve(root) };
5936
8025
  return cachedRepoRoot.root;
5937
8026
  }
5938
8027
  function codeReachRoots(configDir, configured) {
5939
8028
  const repoRoot = repositoryRoot(configDir);
5940
- const resolve5 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path11.resolve(configDir, r));
8029
+ const resolve5 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path12.resolve(configDir, r));
5941
8030
  const global = Array.isArray(configured) ? resolve5(configured) : resolve5(configured?.["*"]);
5942
8031
  const byOrigin = /* @__PURE__ */ new Map();
5943
8032
  if (configured && !Array.isArray(configured)) {
@@ -5974,10 +8063,10 @@ function isFile(file) {
5974
8063
  function resolveSourcePath(source, roots) {
5975
8064
  const normalized = normalizeSourcePath(source);
5976
8065
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
5977
- if (path11.isAbsolute(normalized) && isFile(normalized)) return path11.resolve(normalized);
5978
- const relative8 = normalized.replace(/^\/+/, "");
8066
+ if (path12.isAbsolute(normalized) && isFile(normalized)) return path12.resolve(normalized);
8067
+ const relative6 = normalized.replace(/^\/+/, "");
5979
8068
  for (const root of roots) {
5980
- const candidate = path11.resolve(root, relative8);
8069
+ const candidate = path12.resolve(root, relative6);
5981
8070
  if (isFile(candidate)) return candidate;
5982
8071
  }
5983
8072
  return null;
@@ -6009,7 +8098,7 @@ function fileForUrl(url, roots, bases) {
6009
8098
  return file;
6010
8099
  }
6011
8100
  function relativeToRepo(file, repoRoot) {
6012
- return path11.relative(repoRoot, file).split(path11.sep).join("/");
8101
+ return path12.relative(repoRoot, file).split(path12.sep).join("/");
6013
8102
  }
6014
8103
  function scriptVersion(entry) {
6015
8104
  return entry.hash ?? crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -6072,10 +8161,10 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
6072
8161
  const files = /* @__PURE__ */ new Map();
6073
8162
  const known = /* @__PURE__ */ new Map();
6074
8163
  const addTo = (into, file, functions) => {
6075
- const path15 = relativeToRepo(file, roots.repoRoot);
6076
- const listed = into.get(path15);
8164
+ const path16 = relativeToRepo(file, roots.repoRoot);
8165
+ const listed = into.get(path16);
6077
8166
  if (listed) listed.push(...functions);
6078
- else into.set(path15, [...functions]);
8167
+ else into.set(path16, [...functions]);
6079
8168
  };
6080
8169
  const add2 = (file, functions) => addTo(files, file, functions);
6081
8170
  const bases = viteBases(entries);
@@ -6682,7 +8771,7 @@ async function resolveCssCoverage(sheets, roots, fetchMap) {
6682
8771
  var childProcess = __toESM(require("child_process"));
6683
8772
  var fs11 = __toESM(require("fs"));
6684
8773
  var net3 = __toESM(require("net"));
6685
- var path13 = __toESM(require("path"));
8774
+ var path14 = __toESM(require("path"));
6686
8775
  var import_node_module2 = require("module");
6687
8776
 
6688
8777
  // src/internal/capture/worker-metrics.ts
@@ -6690,7 +8779,7 @@ var import_node_perf_hooks = require("perf_hooks");
6690
8779
 
6691
8780
  // src/internal/support/system-readers.ts
6692
8781
  var fs10 = __toESM(require("fs"));
6693
- var path12 = __toESM(require("path"));
8782
+ var path13 = __toESM(require("path"));
6694
8783
  var CLK_TCK = 100;
6695
8784
  function readText(file) {
6696
8785
  try {
@@ -6776,7 +8865,7 @@ var ProcFs = class {
6776
8865
  }
6777
8866
  /** Whether this looks like a Linux `/proc` at all. */
6778
8867
  available() {
6779
- return fs10.existsSync(path12.join(this.root, "stat"));
8868
+ return fs10.existsSync(path13.join(this.root, "stat"));
6780
8869
  }
6781
8870
  /** Every process, keyed by pid. */
6782
8871
  table() {
@@ -6789,32 +8878,32 @@ var ProcFs = class {
6789
8878
  }
6790
8879
  for (const name of names) {
6791
8880
  if (!/^\d+$/.test(name)) continue;
6792
- const text = readText(path12.join(this.root, name, "stat"));
8881
+ const text = readText(path13.join(this.root, name, "stat"));
6793
8882
  const stat = text ? parseProcStat(Number(name), text) : null;
6794
8883
  if (stat) out.set(stat.pid, stat);
6795
8884
  }
6796
8885
  return out;
6797
8886
  }
6798
8887
  stat(pid) {
6799
- const text = readText(path12.join(this.root, String(pid), "stat"));
8888
+ const text = readText(path13.join(this.root, String(pid), "stat"));
6800
8889
  return text ? parseProcStat(pid, text) : null;
6801
8890
  }
6802
8891
  runWaitMs(pid) {
6803
- const text = readText(path12.join(this.root, String(pid), "schedstat"));
8892
+ const text = readText(path13.join(this.root, String(pid), "schedstat"));
6804
8893
  return text ? parseSchedstat(text) : null;
6805
8894
  }
6806
8895
  cmdline(pid) {
6807
- return (readText(path12.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
8896
+ return (readText(path13.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
6808
8897
  }
6809
8898
  /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
6810
8899
  statusKb(pid, key) {
6811
- const text = readText(path12.join(this.root, String(pid), "status"));
8900
+ const text = readText(path13.join(this.root, String(pid), "status"));
6812
8901
  return text ? parseKbField(text, key) : null;
6813
8902
  }
6814
8903
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
6815
8904
  async pssKb(pid) {
6816
8905
  try {
6817
- return parseKbField(await fs10.promises.readFile(path12.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
8906
+ return parseKbField(await fs10.promises.readFile(path13.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
6818
8907
  } catch {
6819
8908
  return null;
6820
8909
  }
@@ -6822,29 +8911,29 @@ var ProcFs = class {
6822
8911
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
6823
8912
  resetPeakRss(pid) {
6824
8913
  try {
6825
- fs10.writeFileSync(path12.join(this.root, String(pid), "clear_refs"), "5");
8914
+ fs10.writeFileSync(path13.join(this.root, String(pid), "clear_refs"), "5");
6826
8915
  return true;
6827
8916
  } catch {
6828
8917
  return false;
6829
8918
  }
6830
8919
  }
6831
8920
  cpuTicks() {
6832
- const text = readText(path12.join(this.root, "stat"));
8921
+ const text = readText(path13.join(this.root, "stat"));
6833
8922
  return text ? parseCpuTicks(text) : null;
6834
8923
  }
6835
8924
  /** Total stall time of a resource's pressure file, in µs. */
6836
8925
  pressureUs(resource) {
6837
- const text = readText(path12.join(this.root, "pressure", resource));
8926
+ const text = readText(path13.join(this.root, "pressure", resource));
6838
8927
  return text ? parsePsiTotal(text) : null;
6839
8928
  }
6840
8929
  memAvailableKb() {
6841
- const text = readText(path12.join(this.root, "meminfo"));
8930
+ const text = readText(path13.join(this.root, "meminfo"));
6842
8931
  return text ? parseKbField(text, "MemAvailable") : null;
6843
8932
  }
6844
8933
  /** Open file descriptors of a process. */
6845
8934
  fdCount(pid) {
6846
8935
  try {
6847
- return fs10.readdirSync(path12.join(this.root, String(pid), "fd")).length;
8936
+ return fs10.readdirSync(path13.join(this.root, String(pid), "fd")).length;
6848
8937
  } catch {
6849
8938
  return null;
6850
8939
  }
@@ -7059,19 +9148,19 @@ var PAGE_READ_CAP = 50;
7059
9148
  var PAGE_READ_TIMEOUT_MS = 1e3;
7060
9149
  var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
7061
9150
  var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
7062
- function relativePath(file) {
7063
- return path13.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path13.sep).join("/");
9151
+ function relativePath(file, cwd = process.cwd(), windows = process.platform === "win32") {
9152
+ return callSitePath(file, cwd, windows) ?? file;
7064
9153
  }
7065
9154
  function isLibraryFile(file) {
7066
- return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path13.sep}node_modules${path13.sep}`) || file.includes("/node_modules/") || file === __filename;
9155
+ return file.startsWith("node:") || file.startsWith("internal/") || /[\\/]node_modules[\\/]/.test(file) || file === __filename;
7067
9156
  }
7068
- function userSite(stack) {
9157
+ function userSite(stack, cwd = process.cwd(), windows = process.platform === "win32") {
7069
9158
  for (const line of (stack ?? "").split("\n").slice(1)) {
7070
9159
  const match = /^\s*at\s+(?:.*?\s+\()?(.+?):(\d+):\d+\)?$/.exec(line);
7071
9160
  if (!match) continue;
7072
- const file = match[1].replace(/^file:\/\//, "");
7073
- if (isLibraryFile(file) || !/\.[a-z]+$/i.test(file)) continue;
7074
- return `${relativePath(file)}:${match[2]}`;
9161
+ const file = framePath(match[1], windows);
9162
+ if (!file || isLibraryFile(file) || !/\.[a-z]+$/i.test(file)) continue;
9163
+ return `${relativePath(file, cwd, windows)}:${match[2]}`;
7075
9164
  }
7076
9165
  return null;
7077
9166
  }
@@ -7111,7 +9200,7 @@ function playwrightTestInfoSource() {
7111
9200
  try {
7112
9201
  const packageJson = resolvePlaywrightPackageJson();
7113
9202
  if (!packageJson) return () => null;
7114
- const globals = (0, import_node_module2.createRequire)(packageJson)(path13.join(path13.dirname(packageJson), "lib", "globals.js"));
9203
+ const globals = (0, import_node_module2.createRequire)(packageJson)(path14.join(path14.dirname(packageJson), "lib", "globals.js"));
7115
9204
  const read = globals.currentTestInfo;
7116
9205
  if (typeof read !== "function") return () => null;
7117
9206
  return () => {
@@ -7154,7 +9243,7 @@ function deepUserSite() {
7154
9243
  }
7155
9244
  var SHELLS = /^(?:sh|bash|dash|zsh|cmd|cmd\.exe|powershell|powershell\.exe|pwsh)$/i;
7156
9245
  function commandOf(spawnfile, spawnargs) {
7157
- const name = (file) => path13.basename(file.replace(/\\/g, "/").replace(/^["']|["']$/g, ""));
9246
+ const name = (file) => path14.basename(file.replace(/\\/g, "/").replace(/^["']|["']$/g, ""));
7158
9247
  const program = typeof spawnfile === "string" ? name(spawnfile) : "";
7159
9248
  const args = Array.isArray(spawnargs) ? spawnargs.map(String) : [];
7160
9249
  const script = args.findIndex((arg) => arg === "-c" || /^\/c$/i.test(arg));
@@ -7297,17 +9386,17 @@ var ResourceLedger = class {
7297
9386
  onServer(server, site) {
7298
9387
  const known = this.byHandle.get(server);
7299
9388
  if (known && this.handles.has(known)) return;
7300
- let bound = false;
9389
+ let bound2 = false;
7301
9390
  let closed = false;
7302
9391
  const entry = this.trackHandle(server, {
7303
9392
  type: "TCPServerWrap",
7304
9393
  site,
7305
9394
  // `close()` unbinds at once, while its 'close' event waits for open connections.
7306
9395
  running: () => server.listening,
7307
- pending: () => !bound && !closed && !server.listening
9396
+ pending: () => !bound2 && !closed && !server.listening
7308
9397
  });
7309
9398
  server.once("listening", () => {
7310
- bound = true;
9399
+ bound2 = true;
7311
9400
  });
7312
9401
  server.once("close", () => {
7313
9402
  closed = true;
@@ -7361,22 +9450,22 @@ var ResourceLedger = class {
7361
9450
  */
7362
9451
  async testEnded(info) {
7363
9452
  const test = testRef2(info) ?? this.window;
7364
- const failed = typeof info.status === "string" && typeof info.expectedStatus === "string" && info.status !== info.expectedStatus;
7365
- const leaks = test && !failed ? this.leaksOf(test.id) : [];
9453
+ const failed2 = typeof info.status === "string" && typeof info.expectedStatus === "string" && info.status !== info.expectedStatus;
9454
+ const leaks = test && !failed2 ? this.leaksOf(test.id) : [];
7366
9455
  if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
7367
- const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
9456
+ const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed2 ? { failed: true } : {} } : null;
7368
9457
  const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
7369
9458
  const handles = {
7370
9459
  start: this.handlesAtStart,
7371
9460
  end: await settledHandles(this.handlesAtStart, this.countedHandleTypes())
7372
9461
  };
7373
- const running = this.trackingHandles && test ? await this.runningOf(test.id) : null;
9462
+ const running2 = this.trackingHandles && test ? await this.runningOf(test.id) : null;
7374
9463
  const census = this.census(ref, handles, await this.readPages());
7375
9464
  if (metrics) census.metrics = metrics;
7376
- if (running) census.running = running;
9465
+ if (running2) census.running = running2;
7377
9466
  if (test) {
7378
9467
  census.openAtStart = this.openAtStart;
7379
- if (!failed) census.leftOpen = leaks.length;
9468
+ if (!failed2) census.leftOpen = leaks.length;
7380
9469
  }
7381
9470
  this.window = null;
7382
9471
  return { census, leaks };
@@ -7426,10 +9515,10 @@ var ResourceLedger = class {
7426
9515
  for (const method of ["launch", "connect", "connectOverCDP"]) {
7427
9516
  const original = type[method];
7428
9517
  if (typeof original !== "function") continue;
7429
- const bound = original.bind(type);
9518
+ const bound2 = original.bind(type);
7430
9519
  type[method] = async (...args) => {
7431
9520
  const site = userSite(new Error().stack);
7432
- const browser = await bound(...args);
9521
+ const browser = await bound2(...args);
7433
9522
  this.guard(() => this.onBrowser(browser, site));
7434
9523
  return browser;
7435
9524
  };
@@ -7506,12 +9595,12 @@ var ResourceLedger = class {
7506
9595
  for (const method of USE_METHODS) {
7507
9596
  const original = methods[method];
7508
9597
  if (typeof original !== "function") continue;
7509
- const bound = original.bind(page);
9598
+ const bound2 = original.bind(page);
7510
9599
  methods[method] = (...args) => {
7511
9600
  const previous = this.usingPage;
7512
9601
  this.usingPage = entry;
7513
9602
  try {
7514
- return bound(...args);
9603
+ return bound2(...args);
7515
9604
  } finally {
7516
9605
  this.usingPage = previous;
7517
9606
  }
@@ -7780,18 +9869,18 @@ function readPageInteraction() {
7780
9869
  return { path: pathOf(node), offset: textOffset(node, node.childNodes[offset] ?? null, 0) };
7781
9870
  };
7782
9871
  const out = {};
7783
- let active = doc.activeElement;
7784
- while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
7785
- if (active && active !== doc.body && active !== doc.documentElement) {
9872
+ let active2 = doc.activeElement;
9873
+ while (active2?.shadowRoot?.activeElement) active2 = active2.shadowRoot.activeElement;
9874
+ if (active2 && active2 !== doc.body && active2 !== doc.documentElement) {
7786
9875
  let visible = false;
7787
9876
  try {
7788
- visible = active.matches(":focus-visible");
9877
+ visible = active2.matches(":focus-visible");
7789
9878
  } catch {
7790
9879
  }
7791
- out.focus = { element: ref(active), visible };
9880
+ out.focus = { element: ref(active2), visible };
7792
9881
  try {
7793
- if (typeof active.selectionStart === "number" && typeof active.selectionEnd === "number") {
7794
- out.focus.selection = [active.selectionStart, active.selectionEnd];
9882
+ if (typeof active2.selectionStart === "number" && typeof active2.selectionEnd === "number") {
9883
+ out.focus.selection = [active2.selectionStart, active2.selectionEnd];
7795
9884
  }
7796
9885
  } catch {
7797
9886
  }
@@ -7806,7 +9895,7 @@ function readPageInteraction() {
7806
9895
  scope = innermost.shadowRoot;
7807
9896
  }
7808
9897
  const selection = doc.getSelection?.();
7809
- if (selection && selection.rangeCount > 0 && !selection.isCollapsed && !(active && isField(active))) {
9898
+ if (selection && selection.rangeCount > 0 && !selection.isCollapsed && !(active2 && isField(active2))) {
7810
9899
  const range = selection.getRangeAt(0);
7811
9900
  const start = range.startContainer.nodeType === 3 ? range.startContainer.parentElement : range.startContainer;
7812
9901
  const end = range.endContainer.nodeType === 3 ? range.endContainer.parentElement : range.endContainer;
@@ -7868,7 +9957,7 @@ function observeApiRequestContext(context, recorder, baseURL = null) {
7868
9957
  return proxy;
7869
9958
  }
7870
9959
  function sendingMethod(target, prop, recorder, baseURL) {
7871
- return function send(...args) {
9960
+ return function send2(...args) {
7872
9961
  const record = recorder();
7873
9962
  if (!record) return callMethod(target, prop, args);
7874
9963
  const startedAt = Date.now();
@@ -8033,6 +10122,10 @@ function createSink() {
8033
10122
  frozenAt: null,
8034
10123
  stopEnds: 0,
8035
10124
  ending: false,
10125
+ functionEnded: false,
10126
+ ended: false,
10127
+ end: new AbortController(),
10128
+ skip: void 0,
8036
10129
  released: [],
8037
10130
  releasedRequests: /* @__PURE__ */ new Set(),
8038
10131
  onReleasedRequestsEnded: null,
@@ -8080,7 +10173,7 @@ function configuredCodeReachRoots() {
8080
10173
  }
8081
10174
  } catch {
8082
10175
  }
8083
- return raw.split(path14.delimiter).filter(Boolean);
10176
+ return raw.split(path15.delimiter).filter(Boolean);
8084
10177
  }
8085
10178
  async function stopSinkCodeReach(sink, closing) {
8086
10179
  const stopping = [];
@@ -8431,14 +10524,14 @@ async function maybeOpenPicker(sink, closing) {
8431
10524
  const inspectGate = shouldInspectOnFailure(inspectionGateFromTestInfo(testInfo, process.env.PIWI_INSPECT_ON_FAIL));
8432
10525
  if (!pickGate && !inspectGate) return;
8433
10526
  sink.pickOffered = true;
8434
- const failed = sink.failedLocators[sink.failedLocators.length - 1] ?? deriveFailedLocator(testInfo);
8435
- if (!failed && !inspectGate) {
10527
+ const failed2 = sink.failedLocators[sink.failedLocators.length - 1] ?? deriveFailedLocator(testInfo);
10528
+ if (!failed2 && !inspectGate) {
8436
10529
  console.log("[piwi] locator picker: no failing locator could be identified in this failure \u2014 nothing to replace.");
8437
10530
  return;
8438
10531
  }
8439
10532
  const pick = await internalCall(
8440
10533
  page,
8441
- () => runLocatorPicker(page, testInfo, failed, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG })
10534
+ () => runLocatorPicker(page, testInfo, failed2, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG })
8442
10535
  );
8443
10536
  if (!pick) return;
8444
10537
  sink.userPick = pick;
@@ -8573,7 +10666,7 @@ function knowLocatorCall(sink, page, target, location, origin) {
8573
10666
  let settle = () => {
8574
10667
  };
8575
10668
  const settled = new Promise((resolve5) => settle = resolve5);
8576
- const call = { page, target, location, origin, picked: false, settled, settle };
10669
+ const call = { page, target, location, origin, picked: false, endable: false, settled, settle };
8577
10670
  sink.locatorCalls.set(callSiteLine(location), call);
8578
10671
  return call;
8579
10672
  }
@@ -8640,6 +10733,10 @@ function onDebuggerStop(context) {
8640
10733
  return;
8641
10734
  }
8642
10735
  if (pagePause) stopShown(state);
10736
+ if (sink.ended) {
10737
+ how = goOn(state, "end");
10738
+ return;
10739
+ }
8643
10740
  if (sink.ending || CLOSING_CONTEXTS.has(context)) {
8644
10741
  askAgain(state, asked);
8645
10742
  how = goOn(state, sink.ending ? endedWaiting(sink, call) : void 0);
@@ -8648,6 +10745,7 @@ function onDebuggerStop(context) {
8648
10745
  const of = stopPageOf(context, call, pagePause?.page);
8649
10746
  const ends = sink.stopEnds;
8650
10747
  const shown2 = stopTurn.then(async () => {
10748
+ if (sink.ended) return "end";
8651
10749
  const ended = currentSink !== sink || sink.ending || sink.stopEnds !== ends;
8652
10750
  const gone = decided.show === "breakpoint" && !atBreakpoint(state, callSite);
8653
10751
  if (!ended && !gone && stopCanShow(context, of)) {
@@ -8675,7 +10773,7 @@ function endedWaiting(sink, call) {
8675
10773
  return "finish";
8676
10774
  }
8677
10775
  function stopSite(sink, location) {
8678
- if (path14.dirname(path14.resolve(location.file)) !== __dirname) return stopCallSite(location);
10776
+ if (path15.dirname(path15.resolve(location.file)) !== __dirname) return stopCallSite(location);
8679
10777
  return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
8680
10778
  }
8681
10779
  function stopBarPage(sink, context, of) {
@@ -8694,7 +10792,7 @@ async function showStop(sink, context, title, location, reason, call, of, failur
8694
10792
  const place = runChannelOpen() ? callSitePlace(location) : null;
8695
10793
  if (!bar && !place) return void 0;
8696
10794
  const failing = failure?.failed ?? null;
8697
- const onPick = async (picked) => {
10795
+ const pickTaken = async (picked) => {
8698
10796
  if (call) {
8699
10797
  call.picked = true;
8700
10798
  const { method, args } = call.origin;
@@ -8713,17 +10811,50 @@ async function showStop(sink, context, title, location, reason, call, of, failur
8713
10811
  applyPickToSnapshots(sink.capturedLocators, pick);
8714
10812
  }
8715
10813
  console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
8716
- await postPickToEditor(editorSendAddress(), pickPayload(picked.picked.locator, location));
10814
+ return postPickToEditor(editorSendAddress(), pickPayload(picked.picked.locator, location), fetch, runHost());
8717
10815
  };
10816
+ const onPick = async (picked) => void await pickTaken(picked);
10817
+ const probe = { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG };
8718
10818
  const handoffReason = reason === "page-pause" ? "pause" : "breakpoint";
8719
- const onRecord = bar && page ? recorderAt(page, testInfo, location, handoffReason) : void 0;
8720
- const intents = bar && page ? new BarIntents() : null;
10819
+ const onRecord = page ? recorderAt(page, testInfo, location, handoffReason) : void 0;
10820
+ const intents = page ? new BarIntents() : null;
8721
10821
  const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
8722
10822
  sink.stops.push(record);
10823
+ const stoppedAt = callSitePlace(location);
10824
+ const goneOn = liveViewStopped({
10825
+ reason,
10826
+ call: { title, file: stoppedAt?.file ?? null, line: stoppedAt?.line ?? null },
10827
+ test: testInfo.title
10828
+ });
8723
10829
  const end = new AbortController();
8724
10830
  const over = new AbortController();
8725
10831
  end.signal.addEventListener("abort", () => over.abort(), { once: true });
8726
10832
  let fromEditor;
10833
+ const pickAt = page && !guessed ? async (x, y) => {
10834
+ try {
10835
+ const found = await internalCall(page, () => pickAtPoint(page, probe, x, y));
10836
+ if ("refusal" in found) return { ok: false, message: found.refusal, locator: null };
10837
+ if (over.signal.aborted) return { ok: false, message: "The test went on.", locator: null };
10838
+ const locator = found.picked.picked.locator;
10839
+ const sent2 = await pickTaken(found.picked);
10840
+ return {
10841
+ ok: true,
10842
+ message: sent2 ? `${locator} went to the editor, in place of the locator on the stopped line.` : `${locator}: no editor took it, as this run names no Send to editor pairing that answers.`,
10843
+ locator
10844
+ };
10845
+ } catch (error) {
10846
+ const message = hideSecretsInText(`The stopped page could not be read: ${errorMessage(error)}`);
10847
+ return { ok: false, message, locator: null };
10848
+ }
10849
+ } : void 0;
10850
+ const use = testInfo.project?.use ?? {};
10851
+ const testIdAttribute = typeof use.testIdAttribute === "string" ? use.testIdAttribute : null;
10852
+ const agentProbe = { fn: probeElementAttrs, arg: { ...CAPTURED_ATTRS_ARG, testIdAttribute } };
10853
+ const agent = page && !guessed ? {
10854
+ snapshot: (depth, image) => internalCall(page, () => readSnapshot(page, depth, image)),
10855
+ refLocator: (ref) => internalCall(page, () => refLocators(page, agentProbe, ref, testIdAttribute)),
10856
+ act: (act) => internalCall(page, () => actOnRef(page, agentProbe, act, testIdAttribute))
10857
+ } : void 0;
8727
10858
  const held2 = place ? holdStop(
8728
10859
  {
8729
10860
  reason,
@@ -8738,20 +10869,31 @@ async function showStop(sink, context, title, location, reason, call, of, failur
8738
10869
  guessed,
8739
10870
  // A failure's stop is held in the teardown it ends with.
8740
10871
  holds: () => !over.signal.aborted && (!!failure || !sink.ending && stopCanShow(context, of))
8741
- }) : void 0
10872
+ }) : void 0,
10873
+ ...pickAt ? { pickAt } : {},
10874
+ ...agent ? { agent } : {},
10875
+ browser: context.browser()
8742
10876
  },
8743
10877
  (answer2) => {
8744
10878
  fromEditor = answer2;
8745
10879
  over.abort();
8746
10880
  }
8747
10881
  ) : null;
8748
- const onIntent = bar && page && held2 ? (intent, ended) => intent === "assert" ? checkAt(page, testInfo, location, handoffReason, held2.stop, ended) : regionAt(page, location, held2.stop, ended) : void 0;
10882
+ const onIntent = page && held2 ? (intent, ended) => intent === "assert" ? checkAt(page, testInfo, location, handoffReason, held2.stop, ended) : regionAt(page, location, held2.stop, ended) : void 0;
8749
10883
  const shown2 = withTestTimeoutHeld(testInfo, async () => {
8750
10884
  if (!bar || !page) {
8751
10885
  console.log(
8752
- failure ? `[piwi] The test failed at ${callSiteLine(location)}: Resume in the editor lets it end as it failed.` : `[piwi] Paused at ${callSiteLine(location)} before ${title}: Resume, Step or Finish in the editor.`
10886
+ failure ? `[piwi] The test failed at ${callSiteLine(location)}: Resume in ${runHost()} lets it end as it failed.` : `[piwi] Paused at ${callSiteLine(location)} before ${title}: Resume, Step or Finish in ${runHost()}.`
8753
10887
  );
8754
- await abortion(over.signal);
10888
+ if (page && intents && held2) {
10889
+ const stop2 = held2.stop;
10890
+ await internalCall(
10891
+ page,
10892
+ () => viewHere(page, probe, onPick, onRecord, over.signal, intents, onIntent, () => viewEnded(stop2))
10893
+ );
10894
+ } else {
10895
+ await abortion(over.signal);
10896
+ }
8755
10897
  return "resume";
8756
10898
  }
8757
10899
  return internalCall(
@@ -8763,9 +10905,9 @@ async function showStop(sink, context, title, location, reason, call, of, failur
8763
10905
  call: title,
8764
10906
  attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
8765
10907
  target: call?.page === page ? call.target : null,
8766
- ...failure ? { failure: true } : {}
10908
+ ...failure ? { failure: true } : sink.functionEnded ? {} : { end: true }
8767
10909
  },
8768
- { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
10910
+ probe,
8769
10911
  onPick,
8770
10912
  onRecord,
8771
10913
  over.signal,
@@ -8777,17 +10919,65 @@ async function showStop(sink, context, title, location, reason, call, of, failur
8777
10919
  if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
8778
10920
  else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
8779
10921
  sink.shownStops.delete(end);
10922
+ goneOn();
8780
10923
  await held2?.release();
8781
10924
  });
8782
10925
  const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
8783
10926
  const stop = { page, guessed, context, record, done: shown2.catch(() => void 0), heldAt, after: null };
8784
10927
  if (!failure) sink.shownStops.set(end, stop);
8785
10928
  const answer = await shown2;
8786
- if (!end.signal.aborted) return fromEditor ?? answer;
10929
+ if (!end.signal.aborted) {
10930
+ const chosen = fromEditor ?? answer;
10931
+ return chosen === "end" ? endTest(sink, call, title, location, reason) : chosen;
10932
+ }
8787
10933
  if (end.signal.reason === "ended") return "finish";
8788
10934
  await settledWithin(stop.after ? [stop.after] : [], END_STOP_MS);
8789
10935
  return "resume";
8790
10936
  }
10937
+ function endTest(sink, call, title, location, reason) {
10938
+ const line = callSiteLine(location);
10939
+ if (reason === "failure" || sink.functionEnded || !sink.testInfo) {
10940
+ const said2 = reason === "failure" ? `The test failed at ${line}: End the test lets it end as it failed.` : `The test function already ended: End the test at ${line} lets it end as it is.`;
10941
+ console.log(`[piwi] ${said2}`);
10942
+ if (reason !== "failure") tellHub({ type: "notice", message: said2 });
10943
+ return "finish";
10944
+ }
10945
+ sink.ended = true;
10946
+ skipAtFunctionEnd(sink);
10947
+ sink.end.abort(new Error(ENDED_REASON));
10948
+ if (reason === "page-pause" || call?.endable) {
10949
+ const what = reason === "page-pause" ? "nothing after its page.pause() runs" : `${title} never runs`;
10950
+ console.log(`[piwi] Ended the test at ${line}: ${what}, and the test is skipped.`);
10951
+ return "end";
10952
+ }
10953
+ const said = hideSecretsInText(
10954
+ `${title} at ${line} runs before the test ends: Piwi holds no locator call there. The test is skipped at its next locator call, or as it ends.`
10955
+ );
10956
+ console.log(`[piwi] ${said}`);
10957
+ tellHub({ type: "notice", message: said });
10958
+ return "end";
10959
+ }
10960
+ function endsNow(sink) {
10961
+ return sink.ended && !sink.functionEnded;
10962
+ }
10963
+ function skipOf(sink) {
10964
+ if (sink.skip === void 0) {
10965
+ try {
10966
+ sink.testInfo?.skip(true, ENDED_REASON);
10967
+ sink.skip = new Error(`Test is skipped: ${ENDED_REASON}`);
10968
+ } catch (error) {
10969
+ sink.skip = error;
10970
+ }
10971
+ }
10972
+ return sink.skip;
10973
+ }
10974
+ function skipAtFunctionEnd(sink) {
10975
+ const callbacks = sink.testInfo?._onDidFinishTestFunctionCallbacks;
10976
+ if (!(callbacks instanceof Set)) return;
10977
+ callbacks.add(() => {
10978
+ if (sink.testInfo?.status === "passed") throw skipOf(sink);
10979
+ });
10980
+ }
8791
10981
  function intentAt(intents, what, stop) {
8792
10982
  if (!stop.holds()) return { ok: false, message: "The test went on." };
8793
10983
  if (stop.guessed) {
@@ -8879,7 +11069,7 @@ async function holdFailure(sink, fallback) {
8879
11069
  const context = page ? pageContext(page) : null;
8880
11070
  if (!page || !context) return;
8881
11071
  sink.failureHeld = true;
8882
- const failed = sink.failedLocators[sink.failedLocators.length - 1] ?? deriveFailedLocator(testInfo);
11072
+ const failed2 = sink.failedLocators[sink.failedLocators.length - 1] ?? deriveFailedLocator(testInfo);
8883
11073
  const errors = testInfo.errors.length ? testInfo.errors : testInfo.error ? [testInfo.error] : [];
8884
11074
  const declared = (place2) => place2.file === testInfo.file && place2.line === testInfo.line;
8885
11075
  const placed = errors.flatMap((error2) => {
@@ -8887,8 +11077,8 @@ async function holdFailure(sink, fallback) {
8887
11077
  return place2 && !declared(place2) ? [{ error: error2, place: place2 }] : [];
8888
11078
  })[0];
8889
11079
  const error = placed?.error ?? errors[0];
8890
- const place = placed?.place ?? (failed?.location ? null : stepInProgress(testInfo));
8891
- const location = (place ? stopCallSite(place) : null) ?? failed?.location ?? stopCallSite({ file: testInfo.file, line: testInfo.line, column: testInfo.column });
11080
+ const place = placed?.place ?? (failed2?.location ? null : stepInProgress(testInfo));
11081
+ const location = (place ? stopCallSite(place) : null) ?? failed2?.location ?? stopCallSite({ file: testInfo.file, line: testInfo.line, column: testInfo.column });
8892
11082
  if (!location) return;
8893
11083
  const said = stripAnsi(error?.message ?? "").split("\n").find((line) => line.trim()) ?? status;
8894
11084
  if (page === sink.lastActivePage)
@@ -8896,9 +11086,9 @@ async function holdFailure(sink, fallback) {
8896
11086
  sink.frozenAt = Date.now();
8897
11087
  await stopSinkCodeReach(sink, { context });
8898
11088
  const title = hideSecretsInText(`Failed: ${said.trim().slice(0, 300)}`).slice(0, 1e3);
8899
- const errorText = failureError(error?.message ?? error?.value);
8900
- const named = errorText === null ? null : hideSecretsInText(errorText).slice(0, MAX_STOP_ERROR);
8901
- await showStop(sink, context, title, location, "failure", void 0, { page }, { failed, error: named });
11089
+ const errorText2 = failureError(error?.message ?? error?.value);
11090
+ const named = errorText2 === null ? null : hideSecretsInText(errorText2).slice(0, MAX_STOP_ERROR);
11091
+ await showStop(sink, context, title, location, "failure", void 0, { page }, { failed: failed2, error: named });
8902
11092
  }
8903
11093
  var FAILURE_ERROR_LINES = 4;
8904
11094
  function failureError(message) {
@@ -8911,22 +11101,22 @@ function failureError(message) {
8911
11101
  const text = lines.join("\n").slice(0, MAX_STOP_ERROR);
8912
11102
  return text.trim() ? text : null;
8913
11103
  }
8914
- function errorPlace(error) {
11104
+ function errorPlace(error, windows = process.platform === "win32") {
8915
11105
  const given = error?.location;
8916
11106
  if (typeof given?.file === "string" && typeof given.line === "number") {
8917
11107
  return { file: given.file, line: given.line, column: given.column ?? 0 };
8918
11108
  }
8919
11109
  const stack = error?.stack;
8920
11110
  for (const line of typeof stack === "string" ? stack.split("\n") : []) {
8921
- const frame = /^\s*at\s+(?:.*?\s+\()?(?:file:\/\/)?(\/[^():]+|[A-Za-z]:[\\/][^():]+):(\d+):(\d+)\)?$/.exec(line);
8922
- if (!frame) continue;
8923
- const file = frame[1];
8924
- if (/[\\/]node_modules[\\/]/.test(file) || path14.dirname(path14.resolve(file)) === __dirname) continue;
11111
+ const frame = /^\s*at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?$/.exec(line);
11112
+ const file = frame ? framePath(frame[1], windows) : null;
11113
+ if (!frame || !file || !(windows ? path15.win32 : path15.posix).isAbsolute(file)) continue;
11114
+ if (/[\\/]node_modules[\\/]/.test(file) || path15.dirname(path15.resolve(file)) === __dirname) continue;
8925
11115
  return { file, line: Number(frame[2]), column: Number(frame[3]) };
8926
11116
  }
8927
11117
  return null;
8928
11118
  }
8929
- function stepInProgress(testInfo) {
11119
+ function callInProgress(testInfo) {
8930
11120
  let found = null;
8931
11121
  const visit = (steps) => {
8932
11122
  if (!Array.isArray(steps)) return;
@@ -8935,7 +11125,10 @@ function stepInProgress(testInfo) {
8935
11125
  const at = step.stack?.[0];
8936
11126
  const called = step.category === "pw:api" || step.category === "expect" || step.category === "test.step";
8937
11127
  if (called && typeof at?.file === "string" && typeof at.line === "number") {
8938
- found = { file: at.file, line: at.line, column: typeof at.column === "number" ? at.column : 0 };
11128
+ found = {
11129
+ title: typeof step.title === "string" ? step.title : null,
11130
+ place: { file: at.file, line: at.line, column: typeof at.column === "number" ? at.column : 0 }
11131
+ };
8939
11132
  }
8940
11133
  visit(step.steps);
8941
11134
  }
@@ -8943,6 +11136,15 @@ function stepInProgress(testInfo) {
8943
11136
  visit(testInfo._steps);
8944
11137
  return found;
8945
11138
  }
11139
+ function stepInProgress(testInfo) {
11140
+ return callInProgress(testInfo)?.place ?? null;
11141
+ }
11142
+ function runningCall(testInfo) {
11143
+ const call = callInProgress(testInfo);
11144
+ if (!call?.title) return null;
11145
+ const file = path15.relative(process.cwd(), call.place.file).split(path15.sep).join("/");
11146
+ return { title: call.title, file, line: call.place.line };
11147
+ }
8946
11148
  function abortion(signal) {
8947
11149
  return new Promise((resolve5) => {
8948
11150
  if (signal.aborted) resolve5();
@@ -9008,7 +11210,10 @@ function endStopsWithTestFunction(testInfo, sink) {
9008
11210
  if (!(callbacks instanceof Set)) return;
9009
11211
  const theirs = [...callbacks];
9010
11212
  callbacks.clear();
9011
- callbacks.add(() => endShownStops(sink, "ended"));
11213
+ callbacks.add(() => {
11214
+ sink.functionEnded = true;
11215
+ return endShownStops(sink, "ended");
11216
+ });
9012
11217
  for (const callback of theirs) callbacks.add(callback);
9013
11218
  }
9014
11219
  function recorderAt(page, testInfo, location, reason) {
@@ -9042,6 +11247,7 @@ async function pauseInPlace(page, playwrightPause, location) {
9042
11247
  } finally {
9043
11248
  PAGE_PAUSES.delete(context);
9044
11249
  }
11250
+ if (endsNow(sink)) throw skipOf(sink);
9045
11251
  }
9046
11252
  async function leaveToPlaywright(context, playwrightPause) {
9047
11253
  if (context) PLAYWRIGHT_PAUSES.set(context, (PLAYWRIGHT_PAUSES.get(context) ?? 0) + 1);
@@ -9091,14 +11297,20 @@ function wrapLocator(page, locator, originMethod, originArgs) {
9091
11297
  if (prop === EXPECT_METHOD) {
9092
11298
  return async (...callArgs) => {
9093
11299
  const sink = currentSink;
11300
+ if (sink && endsNow(sink)) throw skipOf(sink);
9094
11301
  const expression = typeof callArgs[0] === "string" ? callArgs[0] : "";
9095
11302
  const isNot = Boolean(callArgs[1]?.isNot);
9096
11303
  const callerLocation = sink ? captureCallerLocation() : null;
9097
11304
  if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
9098
11305
  const call = knowLocatorCall(sink, page, target, callerLocation, { method: originMethod, args: originArgs });
11306
+ const ending = sink?.pause && !sink.ended ? withEndSignal(EXPECT_METHOD, callArgs, sink.end.signal) : null;
11307
+ if (call && ending) call.endable = true;
11308
+ const expectArgs = ending ?? callArgs;
9099
11309
  try {
9100
11310
  if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
9101
- return await callMethod(target, prop, callArgs);
11311
+ const result2 = await callMethod(target, prop, expectArgs);
11312
+ if (sink && endsNow(sink)) throw skipOf(sink);
11313
+ return result2;
9102
11314
  }
9103
11315
  sink.lastActivePage = page;
9104
11316
  const used = {
@@ -9112,7 +11324,8 @@ function wrapLocator(page, locator, originMethod, originArgs) {
9112
11324
  seq = sink.capturedLocators.length;
9113
11325
  sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
9114
11326
  }
9115
- const result = await callMethod(target, prop, callArgs);
11327
+ const result = await callMethod(target, prop, expectArgs);
11328
+ if (endsNow(sink)) throw skipOf(sink);
9116
11329
  const matches = result?.matches;
9117
11330
  if (matches === true && !alreadyProbed && !call?.picked) {
9118
11331
  if (callerLocation) sink.probedLocations.add(callerLocation);
@@ -9121,6 +11334,9 @@ function wrapLocator(page, locator, originMethod, originArgs) {
9121
11334
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
9122
11335
  }
9123
11336
  return result;
11337
+ } catch (error) {
11338
+ if (sink && endsNow(sink)) throw skipOf(sink);
11339
+ throw error;
9124
11340
  } finally {
9125
11341
  forgetLocatorCall(sink, call);
9126
11342
  }
@@ -9130,6 +11346,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
9130
11346
  return async (...callArgs) => {
9131
11347
  const sink = currentSink;
9132
11348
  if (!sink) return callMethod(target, prop, callArgs);
11349
+ if (endsNow(sink)) throw skipOf(sink);
9133
11350
  sink.lastActivePage = page;
9134
11351
  const callerLocation = captureCallerLocation();
9135
11352
  recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, String(prop)));
@@ -9139,6 +11356,8 @@ function wrapLocator(page, locator, originMethod, originArgs) {
9139
11356
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
9140
11357
  };
9141
11358
  const call = knowLocatorCall(sink, page, target, callerLocation, used);
11359
+ const ending = sink.pause && !sink.ended ? withEndSignal(String(prop), callArgs, sink.end.signal) : null;
11360
+ if (call && ending) call.endable = true;
9142
11361
  const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
9143
11362
  let seq = -1;
9144
11363
  if (!alreadyProbed) {
@@ -9152,13 +11371,15 @@ function wrapLocator(page, locator, originMethod, originArgs) {
9152
11371
  }
9153
11372
  let result;
9154
11373
  try {
9155
- result = await callMethod(target, prop, callArgs);
11374
+ result = await callMethod(target, prop, ending ?? callArgs);
9156
11375
  } catch (error) {
11376
+ if (endsNow(sink)) throw skipOf(sink);
9157
11377
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
9158
11378
  throw error;
9159
11379
  } finally {
9160
11380
  forgetLocatorCall(sink, call);
9161
11381
  }
11382
+ if (endsNow(sink)) throw skipOf(sink);
9162
11383
  if (!alreadyProbed && !call?.picked) {
9163
11384
  if (callerLocation) sink.probedLocations.add(callerLocation);
9164
11385
  startElementCapture(sink, page, target, seq, callerLocation, used);
@@ -9191,6 +11412,7 @@ function instrumentPage(page) {
9191
11412
  }
9192
11413
  void maybeStartCodeReach(page);
9193
11414
  observeOwnApiRequests(page);
11415
+ watchPageEvents(page);
9194
11416
  const probeSink = currentSink;
9195
11417
  if (probeSink?.probeItem && !probeSink.probeInterception) {
9196
11418
  void installProbeInterception(page, probeSink.probeItem).then((interception) => {
@@ -9216,6 +11438,7 @@ function instrumentPage(page) {
9216
11438
  if (playwrightPause) {
9217
11439
  page.pause = () => pauseInPlace(page, playwrightPause, captureCallerLocation());
9218
11440
  }
11441
+ if (runChannelOpen() || liveViewOpen()) guardScreencast(page);
9219
11442
  if (typeof page.close === "function") {
9220
11443
  const originalClose = page.close.bind(page);
9221
11444
  page.close = async (...args) => {
@@ -9319,7 +11542,7 @@ function instrumentPage(page) {
9319
11542
  sink.releasedRequests.add(request);
9320
11543
  sink.releasedQuietSince = Date.now();
9321
11544
  });
9322
- const onRequestEnd = (request, failed) => {
11545
+ const onRequestEnd = (request, failed2) => {
9323
11546
  const sink = currentSink;
9324
11547
  if (sink?.releasedRequests.delete(request)) {
9325
11548
  sink.releasedQuietSince = Date.now();
@@ -9329,8 +11552,8 @@ function instrumentPage(page) {
9329
11552
  sink.lastActivePage = page;
9330
11553
  const response = sink.openResponses.get(request) ?? null;
9331
11554
  sink.openResponses.delete(request);
9332
- recordRequest(sink, request, failed ? "failed" : "finished", response);
9333
- if (!failed) void keepResponse(request, response);
11555
+ recordRequest(sink, request, failed2 ? "failed" : "finished", response);
11556
+ if (!failed2) void keepResponse(request, response);
9334
11557
  };
9335
11558
  page.on("requestfinished", (request) => onRequestEnd(request, false));
9336
11559
  page.on("requestfailed", (request) => onRequestEnd(request, true));
@@ -9625,8 +11848,8 @@ async function flushSink(sink, testInfo) {
9625
11848
  body: snapshotJson
9626
11849
  });
9627
11850
  }
9628
- const failed = sink.failedLocators[sink.failedLocators.length - 1];
9629
- const suggestion = failed ? suggestLocatorsFromAria(failed, snapshot) : null;
11851
+ const failed2 = sink.failedLocators[sink.failedLocators.length - 1];
11852
+ const suggestion = failed2 ? suggestLocatorsFromAria(failed2, snapshot) : null;
9630
11853
  if (suggestion) {
9631
11854
  testInfo.annotations.push({
9632
11855
  type: LOCATOR_SUGGESTION_ANNOTATION,
@@ -9763,7 +11986,7 @@ var runControl = {
9763
11986
  }
9764
11987
  };
9765
11988
  function runFile(file) {
9766
- return path14.relative(process.cwd(), file).split(path14.sep).join("/") || file;
11989
+ return path15.relative(process.cwd(), file).split(path15.sep).join("/") || file;
9767
11990
  }
9768
11991
  function testBegin(testInfo, cannotPause) {
9769
11992
  const title = testInfo.titlePath.slice(1);
@@ -9793,17 +12016,21 @@ var piwiFixtures = {
9793
12016
  // ledger and records the calls the tests make through it, and torn down after
9794
12017
  // the browser closed, so its census sees what only the worker's shutdown
9795
12018
  // closed. Depends on `playwright` alone, which launches nothing. The worker's
9796
- // run connection to the editor (`PIWI_RUN_CHANNEL`) opens here, before the
9797
- // first test, and closes as the worker ends.
12019
+ // connection to the instance's live view (`PIWI_LIVE_VIEW`) and its run
12020
+ // connection to the editor (`PIWI_RUN_CHANNEL`), which says how the first
12021
+ // stands and shares it, open here, before the first test, and close as the
12022
+ // worker ends.
9798
12023
  piwiResources: [
9799
12024
  async ({ playwright }, use, workerInfo) => {
9800
12025
  boxCaptureFrames();
9801
12026
  startResourceLedger(playwright, workerInfo.workerIndex);
9802
12027
  observeApiRequests(playwright, apiCallRecorder);
9803
- await openRunChannel(workerInfo, runControl);
12028
+ openLiveView(workerInfo);
12029
+ if (await openRunChannel(workerInfo, runControl)) watchCalls(playwright);
9804
12030
  try {
9805
12031
  await use();
9806
12032
  } finally {
12033
+ closeLiveView();
9807
12034
  closeRunChannel();
9808
12035
  stopResourceLedger();
9809
12036
  }
@@ -9827,6 +12054,7 @@ var piwiFixtures = {
9827
12054
  await armStops([pageContext(page)]);
9828
12055
  await awaitFlakeConditions();
9829
12056
  await maybeStartCodeReach(page);
12057
+ liveViewPage(page);
9830
12058
  await use(page);
9831
12059
  const sink = currentSink;
9832
12060
  if (sink) await holdFailure(sink, page);
@@ -9877,6 +12105,7 @@ var piwiFixtures = {
9877
12105
  });
9878
12106
  activeResourceLedger()?.testStarted(testInfo);
9879
12107
  currentSink = sink;
12108
+ liveViewTestBegan(testInfo.title, () => runningCall(testInfo));
9880
12109
  const showRunChannel = hideRunChannel();
9881
12110
  try {
9882
12111
  await armStops(OPEN_CONTEXTS);
@@ -9886,6 +12115,7 @@ var piwiFixtures = {
9886
12115
  sink.ending = true;
9887
12116
  await endShownStops(sink, "ended");
9888
12117
  await awaitReleasedCalls(sink);
12118
+ await liveViewTestEnded();
9889
12119
  showRunChannel();
9890
12120
  currentSink = null;
9891
12121
  if (sink.pause?.pause) pauseNextTest = true;
@@ -9961,6 +12191,7 @@ function extendPiwiFixtures(test) {
9961
12191
  codeReachRoots,
9962
12192
  computeCoreVitals,
9963
12193
  configuredCodeReachRoots,
12194
+ errorPlace,
9964
12195
  extendPiwiFixtures,
9965
12196
  failureError,
9966
12197
  intentAt,