@piwitests/reporter 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -56,9 +56,9 @@ function defaultDesktopConfigPath() {
56
56
  }
57
57
  function readDesktopConfig(filePath) {
58
58
  try {
59
- const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
60
- const url = parsed?.url;
61
- const token = parsed?.token;
59
+ const parsed2 = JSON.parse(fs.readFileSync(filePath, "utf8"));
60
+ const url = parsed2?.url;
61
+ const token = parsed2?.token;
62
62
  if (typeof url !== "string" || !url) return null;
63
63
  if (typeof token !== "string" || !token) return null;
64
64
  return { url, token };
@@ -145,6 +145,10 @@ var PIWI_ENV_KEYS = {
145
145
  aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
146
146
  aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
147
147
  };
148
+ var PIWI_LOCAL_DEBUG_ENV = {
149
+ pauseAt: "PIWI_PAUSE_AT",
150
+ editorSend: "PIWI_EDITOR_SEND"
151
+ };
148
152
  var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
149
153
  var PIWI_DEFAULTED_CAPTURE_ENV = "PIWI_DEFAULTED_CAPTURE";
150
154
  var PIWI_SELECTION_ENV = {
@@ -320,8 +324,8 @@ var Logger = class {
320
324
  // src/internal/transport/http-client.ts
321
325
  function parseErrorMessage(text) {
322
326
  try {
323
- const parsed = JSON.parse(text);
324
- const message = parsed?.message;
327
+ const parsed2 = JSON.parse(text);
328
+ const message = parsed2?.message;
325
329
  return typeof message === "string" ? message : void 0;
326
330
  } catch {
327
331
  return void 0;
@@ -3780,9 +3784,9 @@ function loadAriaSampleSet(projectName) {
3780
3784
  let set = null;
3781
3785
  try {
3782
3786
  const raw = fs11.readFileSync(getAriaSampleFilePath(projectName), "utf8");
3783
- const parsed = JSON.parse(raw);
3784
- if (parsed.projectName === projectName && Array.isArray(parsed.identities)) {
3785
- set = new Set(parsed.identities.filter((x) => typeof x === "string"));
3787
+ const parsed2 = JSON.parse(raw);
3788
+ if (parsed2.projectName === projectName && Array.isArray(parsed2.identities)) {
3789
+ set = new Set(parsed2.identities.filter((x) => typeof x === "string"));
3786
3790
  }
3787
3791
  } catch {
3788
3792
  set = null;
@@ -3827,11 +3831,17 @@ function wildcard(pattern) {
3827
3831
  const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
3828
3832
  return new RegExp(`^${escaped.join(".*")}$`, "i");
3829
3833
  }
3830
- function selectedProjects(projects, argv = process.argv) {
3831
- const names = cliProjectNames(argv);
3832
- if (!names || names.length === 0) return projects;
3833
- const patterns = names.map(wildcard);
3834
- const chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
3834
+ function selectedProjects(projects, argv = process.argv, filtered) {
3835
+ let chosen;
3836
+ if (Array.isArray(filtered)) {
3837
+ const names = new Set(filtered.map((p) => p.name).filter(Boolean));
3838
+ chosen = new Set(projects.filter((p) => filtered.includes(p) || !!p.name && names.has(p.name)));
3839
+ } else {
3840
+ const names = cliProjectNames(argv);
3841
+ if (!names || names.length === 0) return projects;
3842
+ const patterns = names.map(wildcard);
3843
+ chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
3844
+ }
3835
3845
  if (!argv.includes("--no-deps")) {
3836
3846
  const byName = new Map(projects.map((p) => [p.name ?? "", p]));
3837
3847
  const queue = [...chosen];
@@ -3853,8 +3863,9 @@ function asProxy(value) {
3853
3863
  }
3854
3864
  function baseUrlTargets(config, argv = process.argv) {
3855
3865
  const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
3866
+ const filtered = config.projects?.length ? config.filteredProjects : void 0;
3856
3867
  const targets = /* @__PURE__ */ new Map();
3857
- for (const project of selectedProjects(projects, argv)) {
3868
+ for (const project of selectedProjects(projects, argv, filtered)) {
3858
3869
  const url = project.use?.baseURL;
3859
3870
  if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
3860
3871
  const target = targets.get(url);
@@ -3933,14 +3944,14 @@ var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
3933
3944
  var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
3934
3945
  var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
3935
3946
  function parseManifestJson(text) {
3936
- let parsed;
3947
+ let parsed2;
3937
3948
  try {
3938
- parsed = JSON.parse(text);
3949
+ parsed2 = JSON.parse(text);
3939
3950
  } catch {
3940
3951
  return null;
3941
3952
  }
3942
- if (!parsed || typeof parsed !== "object") return null;
3943
- const obj = parsed;
3953
+ if (!parsed2 || typeof parsed2 !== "object") return null;
3954
+ const obj = parsed2;
3944
3955
  const routes = Array.isArray(obj.routes) ? obj.routes.filter((r) => !!r && typeof r === "object").filter((r) => typeof r.method === "string" && typeof r.pattern === "string").map((r) => ({
3945
3956
  method: r.method,
3946
3957
  pattern: r.pattern,
@@ -4144,8 +4155,8 @@ function parseRouteKey(routeKey2) {
4144
4155
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
4145
4156
  }
4146
4157
  function patternToRegex(pattern) {
4147
- const path31 = pattern.split("?")[0] ?? pattern;
4148
- const escaped = path31.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
4158
+ const path32 = pattern.split("?")[0] ?? pattern;
4159
+ const escaped = path32.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
4149
4160
  return new RegExp(`^${escaped}$`);
4150
4161
  }
4151
4162
  function suitePathEqual(a, b) {
@@ -4168,13 +4179,13 @@ function matchProbeItem(plan, test) {
4168
4179
  function requestMatchesRoute(method, url, routeKey2) {
4169
4180
  const { method: rm, pattern } = parseRouteKey(routeKey2);
4170
4181
  if (rm && rm !== method.toUpperCase()) return false;
4171
- let path31;
4182
+ let path32;
4172
4183
  try {
4173
- path31 = new URL(url).pathname;
4184
+ path32 = new URL(url).pathname;
4174
4185
  } catch {
4175
- path31 = url.split("?")[0] ?? url;
4186
+ path32 = url.split("?")[0] ?? url;
4176
4187
  }
4177
- return patternToRegex(pattern).test(path31);
4188
+ return patternToRegex(pattern).test(path32);
4178
4189
  }
4179
4190
  function createProbeState() {
4180
4191
  return { navigated: false, matches: 0 };
@@ -4196,10 +4207,10 @@ function isProbeMode() {
4196
4207
  function loadPlan() {
4197
4208
  if (planCache !== void 0) return planCache;
4198
4209
  planCache = null;
4199
- const path31 = process.env[PIWI_PROBE_ENV.plan];
4200
- if (path31) {
4210
+ const path32 = process.env[PIWI_PROBE_ENV.plan];
4211
+ if (path32) {
4201
4212
  try {
4202
- planCache = JSON.parse(fs14.readFileSync(path31, "utf8"));
4213
+ planCache = JSON.parse(fs14.readFileSync(path32, "utf8"));
4203
4214
  } catch {
4204
4215
  planCache = null;
4205
4216
  }
@@ -4221,10 +4232,10 @@ function outcomeFromStatus(status, applied) {
4221
4232
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
4222
4233
  }
4223
4234
  function recordProbeOutcome(line) {
4224
- const path31 = process.env[PIWI_PROBE_ENV.results];
4225
- if (!path31) return;
4235
+ const path32 = process.env[PIWI_PROBE_ENV.results];
4236
+ if (!path32) return;
4226
4237
  try {
4227
- fs14.appendFileSync(path31, `${JSON.stringify(line)}
4238
+ fs14.appendFileSync(path32, `${JSON.stringify(line)}
4228
4239
  `);
4229
4240
  } catch {
4230
4241
  }
@@ -4447,8 +4458,8 @@ var FLAKE_LAB_RUN_METADATA_KEY = "piwiFlakeLab";
4447
4458
  // ../core/src/page-key.ts
4448
4459
  function normalizeRoute(url) {
4449
4460
  try {
4450
- const parsed = new URL(url);
4451
- let pathname = parsed.pathname;
4461
+ const parsed2 = new URL(url);
4462
+ let pathname = parsed2.pathname;
4452
4463
  pathname = pathname.replace(/\/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?=\/|$)/g, "/:jwt");
4453
4464
  pathname = pathname.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?=\/|$)/gi, "/:uuid");
4454
4465
  pathname = pathname.replace(/\/[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}(?=\/|$)/gi, "/:ulid");
@@ -4456,8 +4467,8 @@ function normalizeRoute(url) {
4456
4467
  pathname = pathname.replace(/\/[0-9a-f]{32,}(?=\/|$)/gi, "/:token");
4457
4468
  pathname = pathname.replace(/\/\d+(?=\/|$)/g, "/:id");
4458
4469
  let queryString = "";
4459
- if (parsed.search) {
4460
- const params = new URLSearchParams(parsed.search);
4470
+ if (parsed2.search) {
4471
+ const params = new URLSearchParams(parsed2.search);
4461
4472
  const redacted = new URLSearchParams();
4462
4473
  for (const [key] of params) {
4463
4474
  redacted.set(key, "<redacted>");
@@ -4509,24 +4520,24 @@ function faultRouteAction(fault) {
4509
4520
  return fault === "slow" ? { kind: "delay", ms: SLOW_FAULT_DELAY_MS } : null;
4510
4521
  }
4511
4522
  function dropOneField(body) {
4512
- let parsed;
4523
+ let parsed2;
4513
4524
  try {
4514
- parsed = JSON.parse(body);
4525
+ parsed2 = JSON.parse(body);
4515
4526
  } catch {
4516
4527
  return body;
4517
4528
  }
4518
- if (Array.isArray(parsed)) {
4529
+ if (Array.isArray(parsed2)) {
4519
4530
  let key;
4520
- for (const el of parsed) {
4531
+ for (const el of parsed2) {
4521
4532
  if (el && typeof el === "object" && !Array.isArray(el)) {
4522
4533
  key ??= Object.keys(el)[0];
4523
4534
  if (key) delete el[key];
4524
4535
  }
4525
4536
  }
4526
- return JSON.stringify(parsed);
4537
+ return JSON.stringify(parsed2);
4527
4538
  }
4528
- if (parsed && typeof parsed === "object") {
4529
- const obj = parsed;
4539
+ if (parsed2 && typeof parsed2 === "object") {
4540
+ const obj = parsed2;
4530
4541
  const key = Object.keys(obj)[0];
4531
4542
  if (key) delete obj[key];
4532
4543
  return JSON.stringify(obj);
@@ -5334,8 +5345,8 @@ function ciBuildUrlFromMetadata(metadata) {
5334
5345
 
5335
5346
  // src/internal/submit/run-submitter.ts
5336
5347
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
5337
- function markSubmitFallback(run, path31, reason) {
5338
- run.metadata.ingestHealth = { submitFallback: { path: path31, reason } };
5348
+ function markSubmitFallback(run, path32, reason) {
5349
+ run.metadata.ingestHealth = { submitFallback: { path: path32, reason } };
5339
5350
  }
5340
5351
  var RunSubmitter = class {
5341
5352
  /**
@@ -7895,7 +7906,7 @@ function resourceTimeline(samples, censuses) {
7895
7906
  let startedAt = hasSamples ? samples.startedAt : Infinity;
7896
7907
  for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
7897
7908
  const shift = hasSamples ? samples.startedAt - startedAt : 0;
7898
- const relative13 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
7909
+ const relative14 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
7899
7910
  const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
7900
7911
  const perWorker = Math.max(
7901
7912
  60,
@@ -7903,17 +7914,17 @@ function resourceTimeline(samples, censuses) {
7903
7914
  );
7904
7915
  return {
7905
7916
  startedAt,
7906
- cpuPct: hasSamples ? downsamplePoints(relative13(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
7907
- memoryBytes: hasSamples ? downsamplePoints(relative13(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
7917
+ cpuPct: hasSamples ? downsamplePoints(relative14(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
7918
+ memoryBytes: hasSamples ? downsamplePoints(relative14(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
7908
7919
  pages: workers.map(({ worker, events }) => {
7909
- const points = relative13(events, -startedAt);
7920
+ const points = relative14(events, -startedAt);
7910
7921
  const last = points.pop();
7911
7922
  return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
7912
7923
  }),
7913
7924
  workers: trees.map((tree) => ({
7914
7925
  worker: tree.worker,
7915
- cpuCores: downsamplePoints(relative13(tree.cpuCores, shift), perWorker, "mean"),
7916
- memoryBytes: downsamplePoints(relative13(tree.memoryBytes, shift), perWorker, "max")
7926
+ cpuCores: downsamplePoints(relative14(tree.cpuCores, shift), perWorker, "mean"),
7927
+ memoryBytes: downsamplePoints(relative14(tree.memoryBytes, shift), perWorker, "max")
7917
7928
  }))
7918
7929
  };
7919
7930
  }
@@ -8076,8 +8087,8 @@ function formatTimeout(ms) {
8076
8087
  }
8077
8088
  function routeOf(url) {
8078
8089
  try {
8079
- const parsed = new URL(url);
8080
- return `${parsed.pathname}${parsed.search}` || "/";
8090
+ const parsed2 = new URL(url);
8091
+ return `${parsed2.pathname}${parsed2.search}` || "/";
8081
8092
  } catch {
8082
8093
  return url;
8083
8094
  }
@@ -8097,14 +8108,14 @@ function textOfGetByText(locator) {
8097
8108
  const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
8098
8109
  return m ? m[2] : null;
8099
8110
  }
8100
- function receivedDisplay(parsed) {
8101
- const received = parsed.received ?? "";
8102
- if (parsed.assertion !== "toHaveURL") return received;
8111
+ function receivedDisplay(parsed2) {
8112
+ const received = parsed2.received ?? "";
8113
+ if (parsed2.assertion !== "toHaveURL") return received;
8103
8114
  const m = /^"?(https?:\/\/[^"\s]+)"?$/.exec(received);
8104
8115
  return m ? `"${routeOf(m[1])}"` : received;
8105
8116
  }
8106
- function firstLineFallback(parsed) {
8107
- const line = (parsed.messageHead.split("\n")[0] ?? "").replace(/^Error:\s*/, "").replace(MASK_TOKEN_RE, "\u2026");
8117
+ function firstLineFallback(parsed2) {
8118
+ const line = (parsed2.messageHead.split("\n")[0] ?? "").replace(/^Error:\s*/, "").replace(MASK_TOKEN_RE, "\u2026");
8108
8119
  return truncateValue(line || "Unknown error", HEADLINE_MAX_CHARS);
8109
8120
  }
8110
8121
  var Line = class {
@@ -8141,148 +8152,148 @@ function timeoutSuffix(timeoutMs) {
8141
8152
  function timeoutParen(timeoutMs) {
8142
8153
  return timeoutMs !== null ? ` (${formatTimeout(timeoutMs)})` : "";
8143
8154
  }
8144
- function buildActionTimeout(parsed, opts) {
8155
+ function buildActionTimeout(parsed2, opts) {
8145
8156
  const line = new Line();
8146
- const verb = ACTION_VERBS[parsed.action ?? ""] ?? parsed.action ?? "action";
8147
- const state = STATE_PHRASES[parsed.lastState];
8148
- if (parsed.locator && state) {
8149
- return subject(line, parsed.locator, opts).text(` ${state} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed.timeoutMs));
8157
+ const verb = ACTION_VERBS[parsed2.action ?? ""] ?? parsed2.action ?? "action";
8158
+ const state = STATE_PHRASES[parsed2.lastState];
8159
+ if (parsed2.locator && state) {
8160
+ return subject(line, parsed2.locator, opts).text(` ${state} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
8150
8161
  }
8151
- if (parsed.locator && parsed.lastState === "resolved-count" && parsed.resolvedCount !== null) {
8152
- const count = parsed.resolvedCount;
8153
- return subject(line, parsed.locator, opts).text(` matched ${count === 0 ? "no" : count} ${countNoun(parsed.locator, count)} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed.timeoutMs));
8162
+ if (parsed2.locator && parsed2.lastState === "resolved-count" && parsed2.resolvedCount !== null) {
8163
+ const count = parsed2.resolvedCount;
8164
+ return subject(line, parsed2.locator, opts).text(` matched ${count === 0 ? "no" : count} ${countNoun(parsed2.locator, count)} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
8154
8165
  }
8155
- if (parsed.locator) {
8156
- return line.text(`${verb} on `).locator(opts.locator ?? parsed.locator).text(" timed out").text(timeoutSuffix(parsed.timeoutMs));
8166
+ if (parsed2.locator) {
8167
+ return line.text(`${verb} on `).locator(opts.locator ?? parsed2.locator).text(" timed out").text(timeoutSuffix(parsed2.timeoutMs));
8157
8168
  }
8158
- const call = parsed.subject && parsed.action ? `${parsed.subject}.${parsed.action}` : verb;
8159
- return line.text(`${call} timed out`).text(timeoutSuffix(parsed.timeoutMs));
8169
+ const call = parsed2.subject && parsed2.action ? `${parsed2.subject}.${parsed2.action}` : verb;
8170
+ return line.text(`${call} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
8160
8171
  }
8161
- function buildAssertion(parsed, opts) {
8172
+ function buildAssertion(parsed2, opts) {
8162
8173
  const line = new Line();
8163
- const matcher = parsed.assertion ?? "expect";
8164
- const notFound = parsed.lastState === "not-found" || parsed.lastState === "resolved-count" && parsed.resolvedCount === 0 || /element\(s\) not found/.test(parsed.received ?? "");
8165
- const expected = parsed.expected ? truncateValue(parsed.expected, opts.valueMax) : null;
8166
- const received = parsed.received ? truncateValue(receivedDisplay(parsed), opts.valueMax) : null;
8167
- if (matcher === "toHaveCount" && parsed.locator) {
8168
- const want = Number(parsed.expected);
8169
- const got = notFound ? 0 : Number(parsed.received);
8174
+ const matcher = parsed2.assertion ?? "expect";
8175
+ const notFound = parsed2.lastState === "not-found" || parsed2.lastState === "resolved-count" && parsed2.resolvedCount === 0 || /element\(s\) not found/.test(parsed2.received ?? "");
8176
+ const expected = parsed2.expected ? truncateValue(parsed2.expected, opts.valueMax) : null;
8177
+ const received = parsed2.received ? truncateValue(receivedDisplay(parsed2), opts.valueMax) : null;
8178
+ if (matcher === "toHaveCount" && parsed2.locator) {
8179
+ const want = Number(parsed2.expected);
8180
+ const got = notFound ? 0 : Number(parsed2.received);
8170
8181
  const wantText = Number.isFinite(want) ? String(want) : expected ?? "?";
8171
- const noun2 = countNoun(parsed.locator, Number.isFinite(want) ? want : 2);
8182
+ const noun2 = countNoun(parsed2.locator, Number.isFinite(want) ? want : 2);
8172
8183
  const gotText = Number.isFinite(got) ? got === 0 ? "none" : String(got) : received ?? "none";
8173
- return line.text(`Expected ${wantText} ${noun2}, found ${gotText} \u2014 `).locator(opts.locator ?? parsed.locator).text(" toHaveCount");
8184
+ return line.text(`Expected ${wantText} ${noun2}, found ${gotText} \u2014 `).locator(opts.locator ?? parsed2.locator).text(" toHaveCount");
8174
8185
  }
8175
8186
  const state = STATE_MATCHERS[matcher];
8176
8187
  if (state) {
8177
- const unmet = parsed.negated ? `stayed ${state.met}` : state.unmet;
8178
- if (!parsed.locator) return line.text(`Expected ${state.met}, page ${unmet}`).text(timeoutParen(parsed.timeoutMs));
8188
+ const unmet = parsed2.negated ? `stayed ${state.met}` : state.unmet;
8189
+ if (!parsed2.locator) return line.text(`Expected ${state.met}, page ${unmet}`).text(timeoutParen(parsed2.timeoutMs));
8179
8190
  if (notFound && matcher !== "toBeVisible" && matcher !== "toBeAttached") {
8180
- return subject(line, parsed.locator, opts).text(` was not found on the page \u2014 expected ${state.met}`).text(timeoutParen(parsed.timeoutMs));
8191
+ return subject(line, parsed2.locator, opts).text(` was not found on the page \u2014 expected ${state.met}`).text(timeoutParen(parsed2.timeoutMs));
8181
8192
  }
8182
- return subject(line, parsed.locator, opts).text(` ${unmet}`).text(timeoutParen(parsed.timeoutMs));
8193
+ return subject(line, parsed2.locator, opts).text(` ${unmet}`).text(timeoutParen(parsed2.timeoutMs));
8183
8194
  }
8184
8195
  const noun = VALUE_MATCHERS[matcher];
8185
8196
  if (noun) {
8186
- if (parsed.locator && notFound) {
8187
- return subject(line, parsed.locator, opts).text(" was not found on the page \u2014 expected ").text(`${noun} `).value(expected ?? "").text(timeoutParen(parsed.timeoutMs));
8197
+ if (parsed2.locator && notFound) {
8198
+ return subject(line, parsed2.locator, opts).text(" was not found on the page \u2014 expected ").text(`${noun} `).value(expected ?? "").text(timeoutParen(parsed2.timeoutMs));
8188
8199
  }
8189
8200
  line.text(`Expected ${noun} `);
8190
8201
  if (expected) line.value(expected);
8191
8202
  else line.text("to match");
8192
8203
  if (received) line.text(", got ").value(received);
8193
- if (parsed.locator)
8194
- line.text(" \u2014 ").locator(opts.locator ?? parsed.locator).text(` ${matcher}`);
8204
+ if (parsed2.locator)
8205
+ line.text(" \u2014 ").locator(opts.locator ?? parsed2.locator).text(` ${matcher}`);
8195
8206
  else if (matcher === "toHaveURL" || matcher === "toHaveTitle") line.text(` \u2014 page ${matcher}`);
8196
8207
  return line;
8197
8208
  }
8198
- if (matcher === "toPass") return line.text("expect.toPass never passed").text(timeoutParen(parsed.timeoutMs));
8209
+ if (matcher === "toPass") return line.text("expect.toPass never passed").text(timeoutParen(parsed2.timeoutMs));
8199
8210
  if (expected || received) {
8200
8211
  line.text("Expected ");
8201
8212
  if (expected) line.value(expected);
8202
8213
  else line.text("a different value");
8203
8214
  if (received) line.text(", got ").value(received);
8204
8215
  line.text(` \u2014 ${matcher}`);
8205
- if (parsed.locator) line.text(" on ").locator(opts.locator ?? parsed.locator);
8216
+ if (parsed2.locator) line.text(" on ").locator(opts.locator ?? parsed2.locator);
8206
8217
  return line;
8207
8218
  }
8208
- if (parsed.locator) {
8209
- return subject(line, parsed.locator, opts).text(` failed ${matcher}`).text(timeoutParen(parsed.timeoutMs));
8219
+ if (parsed2.locator) {
8220
+ return subject(line, parsed2.locator, opts).text(` failed ${matcher}`).text(timeoutParen(parsed2.timeoutMs));
8210
8221
  }
8211
8222
  return line.text(`${matcher} assertion failed`);
8212
8223
  }
8213
- function buildStrictMode(parsed, opts) {
8224
+ function buildStrictMode(parsed2, opts) {
8214
8225
  const line = new Line();
8215
- const count = parsed.resolvedCount ?? 0;
8216
- if (!parsed.locator) return line.text(`Locator matched ${count} elements \u2014 strict mode`);
8217
- return line.locator(opts.locator ?? parsed.locator).text(` matched ${count} ${countNoun(parsed.locator, count)} \u2014 strict mode`);
8226
+ const count = parsed2.resolvedCount ?? 0;
8227
+ if (!parsed2.locator) return line.text(`Locator matched ${count} elements \u2014 strict mode`);
8228
+ return line.locator(opts.locator ?? parsed2.locator).text(` matched ${count} ${countNoun(parsed2.locator, count)} \u2014 strict mode`);
8218
8229
  }
8219
- function buildNavigation(parsed, opts) {
8230
+ function buildNavigation(parsed2, opts) {
8220
8231
  const line = new Line();
8221
- const code = parsed.networkErrorCode?.replace(/^net::/, "") ?? null;
8232
+ const code = parsed2.networkErrorCode?.replace(/^net::/, "") ?? null;
8222
8233
  if (code) {
8223
8234
  const phrase = NETWORK_ERRORS[code] ?? `${code.replace(/^(?:ERR|NS_ERROR)_/, "").replace(/_/g, " ").toLowerCase().replace(/^\w/, (c) => c.toUpperCase())} loading`;
8224
8235
  line.text(phrase);
8225
- if (parsed.url) line.text(" ").value(truncateValue(parsed.url, opts.valueMax * 2));
8236
+ if (parsed2.url) line.text(" ").value(truncateValue(parsed2.url, opts.valueMax * 2));
8226
8237
  return line;
8227
8238
  }
8228
- const route = parsed.url ? routeOf(parsed.url) : null;
8229
- const timedOut = parsed.timeoutMs !== null || parsed.errorName === "TimeoutError";
8230
- if (parsed.action === "waitForURL" || parsed.action === "waitForNavigation") {
8239
+ const route = parsed2.url ? routeOf(parsed2.url) : null;
8240
+ const timedOut = parsed2.timeoutMs !== null || parsed2.errorName === "TimeoutError";
8241
+ if (parsed2.action === "waitForURL" || parsed2.action === "waitForNavigation") {
8231
8242
  line.text("Never navigated to ");
8232
8243
  if (route) line.value(truncateValue(route, opts.valueMax));
8233
8244
  else line.text("the expected URL");
8234
- return line.text(` \u2014 ${parsed.action} timed out`).text(timeoutSuffix(parsed.timeoutMs));
8245
+ return line.text(` \u2014 ${parsed2.action} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
8235
8246
  }
8236
8247
  line.text("Navigation");
8237
8248
  if (route) line.text(" to ").value(truncateValue(route, opts.valueMax));
8238
- if (timedOut) return line.text(" timed out").text(timeoutSuffix(parsed.timeoutMs));
8249
+ if (timedOut) return line.text(" timed out").text(timeoutSuffix(parsed2.timeoutMs));
8239
8250
  return line.text(" failed");
8240
8251
  }
8241
- function buildTestTimeout(parsed, opts, ctx) {
8242
- const line = new Line().text("Test timed out").text(timeoutSuffix(parsed.timeoutMs));
8243
- if (parsed.timeoutPhase) {
8244
- const phase = parsed.timeoutPhase;
8252
+ function buildTestTimeout(parsed2, opts, ctx) {
8253
+ const line = new Line().text("Test timed out").text(timeoutSuffix(parsed2.timeoutMs));
8254
+ if (parsed2.timeoutPhase) {
8255
+ const phase = parsed2.timeoutPhase;
8245
8256
  const isHook = /^(?:before|after)(?:Each|All)$/.test(phase);
8246
8257
  return line.text(isHook ? ` in the "${phase}" hook` : ` while tearing down "${phase}"`);
8247
8258
  }
8248
- if (parsed.isNavigationFailure && parsed.url) {
8249
- return line.text(" while navigating to ").value(truncateValue(routeOf(parsed.url), opts.valueMax));
8259
+ if (parsed2.isNavigationFailure && parsed2.url) {
8260
+ return line.text(" while navigating to ").value(truncateValue(routeOf(parsed2.url), opts.valueMax));
8250
8261
  }
8251
- if (parsed.assertion && parsed.locator) {
8252
- const state = STATE_MATCHERS[parsed.assertion];
8262
+ if (parsed2.assertion && parsed2.locator) {
8263
+ const state = STATE_MATCHERS[parsed2.assertion];
8253
8264
  line.text(state ? ` while waiting for ` : " while expecting ");
8254
- subject(line, parsed.locator, opts);
8255
- return line.text(state ? ` to be ${state.met}` : ` ${parsed.assertion}`);
8265
+ subject(line, parsed2.locator, opts);
8266
+ return line.text(state ? ` to be ${state.met}` : ` ${parsed2.assertion}`);
8256
8267
  }
8257
- if (parsed.action && parsed.locator) {
8258
- const gerund = ACTION_GERUNDS[parsed.action] ?? `${ACTION_VERBS[parsed.action] ?? parsed.action} on`;
8268
+ if (parsed2.action && parsed2.locator) {
8269
+ const gerund = ACTION_GERUNDS[parsed2.action] ?? `${ACTION_VERBS[parsed2.action] ?? parsed2.action} on`;
8259
8270
  line.text(` while ${gerund} `);
8260
- return subject(line, parsed.locator, opts);
8271
+ return subject(line, parsed2.locator, opts);
8261
8272
  }
8262
- if (parsed.subject && parsed.action) return line.text(` during ${parsed.subject}.${parsed.action}`);
8273
+ if (parsed2.subject && parsed2.action) return line.text(` during ${parsed2.subject}.${parsed2.action}`);
8263
8274
  const step = ctx?.lastStepTitle?.trim();
8264
8275
  if (step) return line.text(" while ").value(`"${truncateValue(step, opts.valueMax * 1.5)}"`);
8265
8276
  return line;
8266
8277
  }
8267
- function buildCrash(parsed, opts) {
8278
+ function buildCrash(parsed2, opts) {
8268
8279
  const line = new Line();
8269
- const what = /Page crashed/i.test(parsed.messageHead) ? "Page crashed" : "Page or browser closed";
8270
- if (parsed.action) {
8271
- const verb = ACTION_VERBS[parsed.action] ?? parsed.action;
8280
+ const what = /Page crashed/i.test(parsed2.messageHead) ? "Page crashed" : "Page or browser closed";
8281
+ if (parsed2.action) {
8282
+ const verb = ACTION_VERBS[parsed2.action] ?? parsed2.action;
8272
8283
  line.text(`${what} during ${verb}`);
8273
- if (parsed.locator) line.text(" on ").locator(opts.locator ?? parsed.locator);
8284
+ if (parsed2.locator) line.text(" on ").locator(opts.locator ?? parsed2.locator);
8274
8285
  return line;
8275
8286
  }
8276
- if (parsed.assertion && parsed.locator) {
8277
- return line.text(`${what} while expecting `).locator(opts.locator ?? parsed.locator).text(` ${parsed.assertion}`);
8287
+ if (parsed2.assertion && parsed2.locator) {
8288
+ return line.text(`${what} while expecting `).locator(opts.locator ?? parsed2.locator).text(` ${parsed2.assertion}`);
8278
8289
  }
8279
8290
  return line.text(what);
8280
8291
  }
8281
8292
  var CUSTOM_MESSAGE_MAX_CHARS = 60;
8282
8293
  var SENTENCE_CASE_WORD_RE = /^[A-Z][a-z]*(?![A-Za-z])/;
8283
- function withLeads(line, parsed, ctx) {
8284
- const hook = ctx?.failedIn && !parsed.timeoutPhase ? ctx.failedIn.hook : null;
8285
- const message = parsed.customMessage ? truncateValue(parsed.customMessage, CUSTOM_MESSAGE_MAX_CHARS) : null;
8294
+ function withLeads(line, parsed2, ctx) {
8295
+ const hook = ctx?.failedIn && !parsed2.timeoutPhase ? ctx.failedIn.hook : null;
8296
+ const message = parsed2.customMessage ? truncateValue(parsed2.customMessage, CUSTOM_MESSAGE_MAX_CHARS) : null;
8286
8297
  if (!hook && !message) return line;
8287
8298
  const led = new Line();
8288
8299
  if (hook) led.text(`In ${hook}${message ? ", " : ": "}`);
@@ -8295,49 +8306,49 @@ function withLeads(line, parsed, ctx) {
8295
8306
  });
8296
8307
  return led;
8297
8308
  }
8298
- function build(parsed, opts, ctx) {
8299
- return withLeads(buildLine(parsed, opts, ctx), parsed, ctx);
8309
+ function build(parsed2, opts, ctx) {
8310
+ return withLeads(buildLine(parsed2, opts, ctx), parsed2, ctx);
8300
8311
  }
8301
- function buildLine(parsed, opts, ctx) {
8302
- switch (parsed.kind) {
8312
+ function buildLine(parsed2, opts, ctx) {
8313
+ switch (parsed2.kind) {
8303
8314
  case "action-timeout":
8304
- return buildActionTimeout(parsed, opts);
8315
+ return buildActionTimeout(parsed2, opts);
8305
8316
  case "assertion":
8306
8317
  case "assertion-timeout":
8307
- return buildAssertion(parsed, opts);
8318
+ return buildAssertion(parsed2, opts);
8308
8319
  case "strict-mode":
8309
- return buildStrictMode(parsed, opts);
8320
+ return buildStrictMode(parsed2, opts);
8310
8321
  case "navigation":
8311
- return buildNavigation(parsed, opts);
8322
+ return buildNavigation(parsed2, opts);
8312
8323
  case "test-timeout":
8313
- return buildTestTimeout(parsed, opts, ctx);
8324
+ return buildTestTimeout(parsed2, opts, ctx);
8314
8325
  case "crash":
8315
- return buildCrash(parsed, opts);
8326
+ return buildCrash(parsed2, opts);
8316
8327
  default:
8317
- return new Line().text(firstLineFallback(parsed));
8328
+ return new Line().text(firstLineFallback(parsed2));
8318
8329
  }
8319
8330
  }
8320
- function detailOf(parsed, headline) {
8321
- switch (parsed.kind) {
8331
+ function detailOf(parsed2, headline) {
8332
+ switch (parsed2.kind) {
8322
8333
  case "test-timeout":
8323
8334
  case "action-timeout": {
8324
- const state = parsed.lastStateLine;
8325
- if (!state || parsed.lastState === "not-found" || headline.includes(state)) return null;
8335
+ const state = parsed2.lastStateLine;
8336
+ if (!state || parsed2.lastState === "not-found" || headline.includes(state)) return null;
8326
8337
  return truncateValue(state, HEADLINE_MAX_CHARS);
8327
8338
  }
8328
8339
  case "assertion":
8329
8340
  case "assertion-timeout": {
8330
- const received = parsed.received;
8341
+ const received = parsed2.received;
8331
8342
  if (received && !/element\(s\) not found/.test(received)) {
8332
- const shown = truncateValue(receivedDisplay(parsed), SHORT_VALUE_MAX_CHARS).slice(0, 8);
8343
+ const shown = truncateValue(receivedDisplay(parsed2), SHORT_VALUE_MAX_CHARS).slice(0, 8);
8333
8344
  if (!headline.includes(shown)) return truncateValue(`Received: ${received}`, HEADLINE_MAX_CHARS);
8334
8345
  }
8335
- const state = parsed.lastStateLine;
8346
+ const state = parsed2.lastStateLine;
8336
8347
  if (!state || /^(?:waiting for |unexpected value )/.test(state)) return null;
8337
8348
  return truncateValue(state, HEADLINE_MAX_CHARS);
8338
8349
  }
8339
8350
  case "navigation":
8340
- return parsed.networkErrorCode ?? parsed.lastStateLine;
8351
+ return parsed2.networkErrorCode ?? parsed2.lastStateLine;
8341
8352
  case "strict-mode":
8342
8353
  case "crash":
8343
8354
  return null;
@@ -8345,16 +8356,16 @@ function detailOf(parsed, headline) {
8345
8356
  return null;
8346
8357
  }
8347
8358
  }
8348
- function describeFailure(parsed, ctx) {
8359
+ function describeFailure(parsed2, ctx) {
8349
8360
  const attempts = [
8350
- { locator: parsed.locator, valueMax: VALUE_MAX_CHARS },
8351
- { locator: parsed.leafLocator ?? parsed.locator, valueMax: VALUE_MAX_CHARS },
8352
- { locator: parsed.leafLocator ?? parsed.locator, valueMax: SHORT_VALUE_MAX_CHARS }
8361
+ { locator: parsed2.locator, valueMax: VALUE_MAX_CHARS },
8362
+ { locator: parsed2.leafLocator ?? parsed2.locator, valueMax: VALUE_MAX_CHARS },
8363
+ { locator: parsed2.leafLocator ?? parsed2.locator, valueMax: SHORT_VALUE_MAX_CHARS }
8353
8364
  ];
8354
- let line = build(parsed, attempts[0], ctx);
8365
+ let line = build(parsed2, attempts[0], ctx);
8355
8366
  for (const opts of attempts.slice(1)) {
8356
8367
  if (line.toString().length <= HEADLINE_MAX_CHARS) break;
8357
- line = build(parsed, opts, ctx);
8368
+ line = build(parsed2, opts, ctx);
8358
8369
  }
8359
8370
  let parts = line.parts.map((p) => ({ ...p, text: p.text.replace(MASK_TOKEN_RE, "\u2026") }));
8360
8371
  let headline = parts.map((p) => p.text).join("");
@@ -8364,10 +8375,10 @@ function describeFailure(parsed, ctx) {
8364
8375
  parts.push({ kind: "text", text: "\u2026" });
8365
8376
  }
8366
8377
  if (!headline.trim()) {
8367
- headline = firstLineFallback(parsed);
8378
+ headline = firstLineFallback(parsed2);
8368
8379
  parts = [{ kind: "text", text: headline }];
8369
8380
  }
8370
- return { headline, detail: detailOf(parsed, headline), parts };
8381
+ return { headline, detail: detailOf(parsed2, headline), parts };
8371
8382
  }
8372
8383
  function clipParts(parts, max) {
8373
8384
  const out = [];
@@ -9077,7 +9088,7 @@ async function resolveSelection(options = {}) {
9077
9088
  }
9078
9089
 
9079
9090
  // src/internal/capture/capture-fixtures.ts
9080
- var path27 = __toESM(require("path"));
9091
+ var path28 = __toESM(require("path"));
9081
9092
  var import_node_zlib2 = require("zlib");
9082
9093
 
9083
9094
  // ../picker-dom/src/probe.ts
@@ -10140,6 +10151,83 @@ function showPickerChoices(arg) {
10140
10151
  doc.body.appendChild(wrap);
10141
10152
  }
10142
10153
 
10154
+ // ../picker-dom/src/overlay-pause.ts
10155
+ function showPauseBar(arg) {
10156
+ const g = globalThis;
10157
+ const doc = g.document;
10158
+ delete g.__piwiPauseState;
10159
+ if (!doc || !doc.body) {
10160
+ g.__piwiPauseState = "resume";
10161
+ return;
10162
+ }
10163
+ if (typeof g.__piwiPauseCleanup === "function") g.__piwiPauseCleanup();
10164
+ const Z = 2147483600;
10165
+ const bar = doc.createElement("div");
10166
+ bar.id = "__piwi-pause-bar";
10167
+ bar.setAttribute("role", "toolbar");
10168
+ bar.setAttribute("aria-label", "Piwi: test paused");
10169
+ bar.style.cssText = `position:fixed;top:0;left:0;right:0;z-index:${Z + 4};display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:8px 12px;background:#111827;color:#f9fafb;border-bottom:2px solid #a78bfa;box-shadow:0 4px 20px rgba(0,0,0,.35);font:13px/1.5 system-ui,sans-serif;`;
10170
+ const where = doc.createElement("div");
10171
+ where.style.cssText = "font-weight:600;white-space:nowrap;";
10172
+ where.textContent = `Paused at ${arg.place}${arg.attempt ? ` \xB7 attempt ${arg.attempt}` : ""}`;
10173
+ const what = doc.createElement("code");
10174
+ what.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#d8b4fe;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;";
10175
+ what.textContent = arg.locator ? `${arg.action} \xB7 ${arg.locator}` : arg.action;
10176
+ what.title = what.textContent;
10177
+ bar.appendChild(where);
10178
+ bar.appendChild(what);
10179
+ const done = (choice) => {
10180
+ cleanup();
10181
+ g.__piwiPauseState = choice;
10182
+ };
10183
+ const onKey = (e) => {
10184
+ if (e.key !== "Escape") return;
10185
+ e.preventDefault();
10186
+ e.stopImmediatePropagation();
10187
+ done("resume");
10188
+ };
10189
+ const cleanup = () => {
10190
+ doc.removeEventListener("keydown", onKey, true);
10191
+ bar.remove();
10192
+ delete g.__piwiPauseCleanup;
10193
+ };
10194
+ const buttons = [
10195
+ ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
10196
+ ["Step", "step", "Pause again at the next action", false],
10197
+ ["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
10198
+ ["Finish", "finish", "Run on and pause no more in this test", false]
10199
+ ];
10200
+ for (const [label, choice, hint, primary] of buttons) {
10201
+ const btn = doc.createElement("button");
10202
+ btn.type = "button";
10203
+ btn.textContent = label;
10204
+ btn.title = hint;
10205
+ btn.style.cssText = "border-radius:6px;padding:4px 12px;cursor:pointer;font:600 12px/1.5 system-ui,sans-serif;" + (primary ? "background:#7c3aed;color:#fff;border:1px solid #7c3aed;" : "background:#1f2937;color:#f3f4f6;border:1px solid #374151;");
10206
+ btn.addEventListener("click", (e) => {
10207
+ e.preventDefault();
10208
+ e.stopImmediatePropagation();
10209
+ done(choice);
10210
+ });
10211
+ bar.appendChild(btn);
10212
+ }
10213
+ g.__piwiPauseCleanup = cleanup;
10214
+ doc.addEventListener("keydown", onKey, true);
10215
+ doc.body.appendChild(bar);
10216
+ }
10217
+ function pauseAnswered() {
10218
+ const g = globalThis;
10219
+ if (g.__piwiPauseState !== void 0) return true;
10220
+ return !g.document?.getElementById("__piwi-pause-bar");
10221
+ }
10222
+ function takePauseState() {
10223
+ const g = globalThis;
10224
+ const state = g.__piwiPauseState;
10225
+ if (typeof g.__piwiPauseCleanup === "function") g.__piwiPauseCleanup();
10226
+ delete g.__piwiPauseCleanup;
10227
+ delete g.__piwiPauseState;
10228
+ return typeof state === "string" ? state : null;
10229
+ }
10230
+
10143
10231
  // ../core/src/locator-generation.ts
10144
10232
  var TAG_TO_ROLE = {
10145
10233
  a: "link",
@@ -11176,11 +11264,11 @@ function deriveFailedLocator(testInfo) {
11176
11264
  ${err.stack ?? ""}`.replace(ANSI_RE2, "");
11177
11265
  const line = /^\s*Locator:\s*(.+)$/m.exec(text);
11178
11266
  if (!line) continue;
11179
- const parsed = parseLeafLocatorExpression(line[1].trim());
11180
- if (!parsed) continue;
11267
+ const parsed2 = parseLeafLocatorExpression(line[1].trim());
11268
+ if (!parsed2) continue;
11181
11269
  const loc = err.location;
11182
11270
  const location = loc ? `${path25.relative(process.cwd(), loc.file).split(path25.sep).join("/")}:${loc.line}:${loc.column}` : null;
11183
- return { method: parsed.method, args: parsed.args, location };
11271
+ return { method: parsed2.method, args: parsed2.args, location };
11184
11272
  }
11185
11273
  return null;
11186
11274
  }
@@ -11202,25 +11290,16 @@ async function cleanupPicker(page) {
11202
11290
  } catch {
11203
11291
  }
11204
11292
  }
11205
- async function runLocatorPicker(page, testInfo, failed, probe) {
11293
+ async function pickLocator(page, probe, failing) {
11206
11294
  try {
11207
- testInfo.setTimeout(0);
11208
- const rendered = failed ? renderFailing(failed) : null;
11209
- console.log(
11210
- `
11211
- [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).`
11212
- );
11213
- const overlayArg = { transport: "global", failing: rendered };
11295
+ const overlayArg = { transport: "global", failing };
11214
11296
  await page.evaluate(installPickerOverlay, overlayArg);
11215
11297
  await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
11216
11298
  timeout: 0,
11217
11299
  polling: 250
11218
11300
  });
11219
11301
  const state = await page.evaluate(() => globalThis.__piwiPickState);
11220
- if (state !== "picked") {
11221
- await cleanupPicker(page);
11222
- return null;
11223
- }
11302
+ if (state !== "picked") return null;
11224
11303
  const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
11225
11304
  const attrs = await handle.evaluate(probe.fn, probe.arg);
11226
11305
  await handle.dispose();
@@ -11258,11 +11337,10 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
11258
11337
  );
11259
11338
  if (ranked.length === 0) {
11260
11339
  console.log("[piwi] No stable locator could be generated for the picked element \u2014 nothing recorded.");
11261
- await cleanupPicker(page);
11262
11340
  return null;
11263
11341
  }
11264
11342
  await page.evaluate(showPickerChoices, {
11265
- failing: rendered,
11343
+ failing,
11266
11344
  choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
11267
11345
  });
11268
11346
  await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
@@ -11270,31 +11348,45 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
11270
11348
  polling: 250
11271
11349
  });
11272
11350
  const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
11273
- await cleanupPicker(page);
11274
11351
  if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
11275
11352
  const picked = { ...ranked[choice], pickedByUser: true };
11276
- const alternatives = [picked, ...ranked.filter((_, i) => i !== choice)];
11277
- const element = {
11278
- tagName: attrs.tagName,
11279
- attributes: attrs.attributes,
11280
- textContent: attrs.textContent,
11281
- accessibleName,
11282
- center: attrs.center,
11283
- ...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
11284
- ...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
11353
+ return {
11354
+ picked,
11355
+ alternatives: [picked, ...ranked.filter((_, i) => i !== choice)],
11356
+ element: {
11357
+ tagName: attrs.tagName,
11358
+ attributes: attrs.attributes,
11359
+ textContent: attrs.textContent,
11360
+ accessibleName,
11361
+ center: attrs.center,
11362
+ ...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
11363
+ ...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
11364
+ },
11365
+ ...anchors.length > 0 ? { anchors } : {}
11285
11366
  };
11367
+ } finally {
11368
+ await cleanupPicker(page);
11369
+ }
11370
+ }
11371
+ async function runLocatorPicker(page, testInfo, failed, probe) {
11372
+ try {
11373
+ testInfo.setTimeout(0);
11374
+ const rendered = failed ? renderFailing(failed) : null;
11375
+ console.log(
11376
+ `
11377
+ [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).`
11378
+ );
11379
+ const pick = await pickLocator(page, probe, rendered);
11380
+ if (!pick) return null;
11286
11381
  const location = failed?.location ?? null;
11287
11382
  console.log(
11288
11383
  failed ? `[piwi] Replacement picked for ${rendered}${location ? ` at ${location}` : ""}:
11289
- [piwi] ${picked.locator}` : `[piwi] Locator picked while inspecting:
11290
- [piwi] ${picked.locator}`
11384
+ [piwi] ${pick.picked.locator}` : `[piwi] Locator picked while inspecting:
11385
+ [piwi] ${pick.picked.locator}`
11291
11386
  );
11292
11387
  return {
11293
11388
  failing: failed ? { method: failed.method, args: failed.args, rendered, location } : null,
11294
- picked,
11295
- alternatives,
11296
- element,
11297
- ...anchors.length > 0 ? { anchors } : {}
11389
+ ...pick
11298
11390
  };
11299
11391
  } catch {
11300
11392
  return null;
@@ -11327,10 +11419,207 @@ function applyPickToSnapshots(snapshots, pick) {
11327
11419
  return true;
11328
11420
  }
11329
11421
 
11422
+ // src/internal/capture/pause-at.ts
11423
+ var path26 = __toESM(require("path"));
11424
+
11425
+ // ../core/src/editor-send.ts
11426
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
11427
+ function parsePairing(text) {
11428
+ const trimmed = text.trim();
11429
+ const hash = trimmed.indexOf("#");
11430
+ if (hash < 0) return null;
11431
+ const url = trimmed.slice(0, hash);
11432
+ const token = trimmed.slice(hash + 1);
11433
+ if (!/^[A-Za-z0-9_-]{16,}$/.test(token)) return null;
11434
+ let parsed2;
11435
+ try {
11436
+ parsed2 = new URL(url);
11437
+ } catch {
11438
+ return null;
11439
+ }
11440
+ if (parsed2.protocol !== "http:" || !LOOPBACK_HOSTS.has(parsed2.hostname) || !parsed2.port) return null;
11441
+ return { url: `${parsed2.origin}${parsed2.pathname}`, token };
11442
+ }
11443
+ function isInsidePath(file) {
11444
+ if (!file || file.length > 1e3 || file.includes("\0")) return false;
11445
+ if (file.startsWith("/") || file.startsWith("\\") || /^[A-Za-z]:/.test(file)) return false;
11446
+ return file.split(/[\\/]/).every((part) => part !== "..");
11447
+ }
11448
+
11449
+ // src/internal/capture/pause-at.ts
11450
+ var PauseSet = class {
11451
+ constructor(lines) {
11452
+ this.lines = lines;
11453
+ }
11454
+ get size() {
11455
+ return this.lines.size;
11456
+ }
11457
+ /** Whether a call site (`file:line:col`, as `captureCallerLocation` writes it) is on one of the lines. */
11458
+ matches(callerLocation) {
11459
+ if (!callerLocation) return false;
11460
+ const col = callerLocation.lastIndexOf(":");
11461
+ return col > 0 && this.lines.has(callerLocation.slice(0, col));
11462
+ }
11463
+ };
11464
+ var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
11465
+ function callSiteFile(file, cwd) {
11466
+ const slashed = file.replace(/\\/g, "/");
11467
+ const absolute = path26.isAbsolute(file) || path26.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
11468
+ let rel = absolute ? path26.relative(cwd, file) : path26.normalize(slashed);
11469
+ rel = rel.split(path26.sep).join("/").replace(/\\/g, "/");
11470
+ if (rel.startsWith("./")) rel = rel.slice(2);
11471
+ return rel;
11472
+ }
11473
+ var ignoredLogged = /* @__PURE__ */ new Set();
11474
+ function parsePauseAt(value, cwd = process.cwd()) {
11475
+ if (!value?.trim()) return EMPTY;
11476
+ const lines = /* @__PURE__ */ new Set();
11477
+ for (const raw of value.split(";")) {
11478
+ const entry = raw.trim();
11479
+ if (!entry) continue;
11480
+ const m = /^(.+):(\d+)$/.exec(entry);
11481
+ const line = m ? Number(m[2]) : 0;
11482
+ if (!m?.[1] || line < 1) {
11483
+ if (!ignoredLogged.has(entry)) {
11484
+ ignoredLogged.add(entry);
11485
+ console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt}: ignoring "${entry}": expected file:line`);
11486
+ }
11487
+ continue;
11488
+ }
11489
+ lines.add(`${callSiteFile(m[1], cwd)}:${line}`);
11490
+ }
11491
+ return lines.size ? new PauseSet(lines) : EMPTY;
11492
+ }
11493
+ var parsed = null;
11494
+ function currentPauseSet() {
11495
+ const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
11496
+ if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
11497
+ return parsed.set;
11498
+ }
11499
+ function pauseGateFromTestInfo(testInfo) {
11500
+ const use = testInfo.project?.use ?? {};
11501
+ return { ci: process.env.CI, headless: use.headless };
11502
+ }
11503
+ function pauseSkipReason(gate) {
11504
+ if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
11505
+ if (gate.headless !== false)
11506
+ return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
11507
+ return null;
11508
+ }
11509
+ function rearmedTimeout(before, pausedMs) {
11510
+ return before > 0 ? before + Math.max(0, Math.round(pausedMs)) : 0;
11511
+ }
11512
+ function pausePlace(callerLocation) {
11513
+ const col = callerLocation.lastIndexOf(":");
11514
+ const fileLine = col > 0 ? callerLocation.slice(0, col) : callerLocation;
11515
+ return fileLine.slice(fileLine.lastIndexOf("/") + 1);
11516
+ }
11517
+ function assertionLabel(expression, isNot) {
11518
+ const [first = "", ...rest] = expression.split(".").filter(Boolean);
11519
+ const name = first + rest.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
11520
+ return `${isNot ? "not." : ""}${name || "expect"}`;
11521
+ }
11522
+ function pickPlace(callerLocation) {
11523
+ const m = /^(.+):(\d+):\d+$/.exec(callerLocation);
11524
+ if (!m) return null;
11525
+ const file = m[1];
11526
+ if (!isInsidePath(file)) return null;
11527
+ return { file, line: Number(m[2]) };
11528
+ }
11529
+ function pickPayload(locator, callerLocation) {
11530
+ const at = pickPlace(callerLocation);
11531
+ return at ? { kind: "locator", text: locator, at } : { kind: "locator", text: locator };
11532
+ }
11533
+ var postFailureLogged = false;
11534
+ async function postPickToEditor(pairingAddress, payload, fetchImpl = fetch) {
11535
+ if (!pairingAddress) return false;
11536
+ const logOnce = (why) => {
11537
+ if (postFailureLogged) return;
11538
+ postFailureLogged = true;
11539
+ console.log(`[piwi] Could not send the picked locator to the editor: ${why}.`);
11540
+ };
11541
+ const pairing = parsePairing(pairingAddress);
11542
+ if (!pairing) {
11543
+ logOnce(`${PIWI_LOCAL_DEBUG_ENV.editorSend} is not a pairing address`);
11544
+ return false;
11545
+ }
11546
+ try {
11547
+ const response = await fetchImpl(pairing.url, {
11548
+ method: "POST",
11549
+ headers: { Authorization: `Bearer ${pairing.token}`, "Content-Type": "application/json" },
11550
+ body: JSON.stringify(payload),
11551
+ signal: AbortSignal.timeout(5e3)
11552
+ });
11553
+ if (response.ok) return true;
11554
+ logOnce(`the editor answered ${response.status}`);
11555
+ } catch (error) {
11556
+ logOnce(error instanceof Error ? error.message : String(error));
11557
+ }
11558
+ return false;
11559
+ }
11560
+ var skipLogged = false;
11561
+ function logPauseSkipOnce(reason) {
11562
+ if (skipLogged) return;
11563
+ skipLogged = true;
11564
+ console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt} is set but breakpoints are skipped: ${reason}`);
11565
+ }
11566
+ var HIGHLIGHT_MS = 2e3;
11567
+ async function pauseHere(page, testInfo, point, probe, onPick) {
11568
+ const before = testInfo.timeout;
11569
+ const started = Date.now();
11570
+ let shown = null;
11571
+ try {
11572
+ testInfo.setTimeout(0);
11573
+ const highlight = point.target?.highlight;
11574
+ if (typeof highlight === "function") {
11575
+ let timer;
11576
+ shown = await Promise.race([
11577
+ Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
11578
+ new Promise((resolve7) => {
11579
+ timer = setTimeout(() => resolve7(null), HIGHLIGHT_MS);
11580
+ })
11581
+ ]);
11582
+ clearTimeout(timer);
11583
+ }
11584
+ const arg = {
11585
+ place: pausePlace(point.location),
11586
+ attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
11587
+ action: point.action,
11588
+ locator: point.locator
11589
+ };
11590
+ console.log(
11591
+ `[piwi] Paused at ${point.location} before ${point.locator ? `${point.action} \xB7 ${point.locator}` : point.action}: Resume, Step, Pick a locator or Finish in the browser (Esc resumes).`
11592
+ );
11593
+ for (; ; ) {
11594
+ await page.evaluate(showPauseBar, arg);
11595
+ await page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 });
11596
+ const choice = await page.evaluate(takePauseState);
11597
+ if (choice === "resume" || choice === "step" || choice === "finish") return choice;
11598
+ if (choice !== "pick") continue;
11599
+ const picked = await pickLocator(page, probe, null).catch(() => null);
11600
+ if (picked) await onPick(picked);
11601
+ }
11602
+ } catch {
11603
+ return "resume";
11604
+ } finally {
11605
+ try {
11606
+ const dispose = shown?.dispose;
11607
+ const hide = page.hideHighlight;
11608
+ if (typeof dispose === "function") await dispose.call(shown);
11609
+ else if (typeof hide === "function") await hide.call(page);
11610
+ } catch {
11611
+ }
11612
+ try {
11613
+ testInfo.setTimeout(rearmedTimeout(before, Date.now() - started));
11614
+ } catch {
11615
+ }
11616
+ }
11617
+ }
11618
+
11330
11619
  // src/internal/capture/code-reach.ts
11331
11620
  var crypto3 = __toESM(require("crypto"));
11332
11621
  var fs22 = __toESM(require("fs"));
11333
- var path26 = __toESM(require("path"));
11622
+ var path27 = __toESM(require("path"));
11334
11623
  var import_node_child_process3 = require("child_process");
11335
11624
 
11336
11625
  // ../core/src/source-map.ts
@@ -11593,12 +11882,12 @@ function repositoryRoot(from) {
11593
11882
  }).trim();
11594
11883
  } catch {
11595
11884
  }
11596
- cachedRepoRoot = { from, root: path26.resolve(root) };
11885
+ cachedRepoRoot = { from, root: path27.resolve(root) };
11597
11886
  return cachedRepoRoot.root;
11598
11887
  }
11599
11888
  function codeReachRoots(configDir, configured) {
11600
11889
  const repoRoot = repositoryRoot(configDir);
11601
- const roots = configured?.length ? configured.map((r) => path26.resolve(configDir, r)) : [configDir];
11890
+ const roots = configured?.length ? configured.map((r) => path27.resolve(configDir, r)) : [configDir];
11602
11891
  return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
11603
11892
  }
11604
11893
  function isFile(file) {
@@ -11611,10 +11900,10 @@ function isFile(file) {
11611
11900
  function resolveSourcePath(source, roots) {
11612
11901
  const normalized = normalizeSourcePath(source);
11613
11902
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
11614
- if (path26.isAbsolute(normalized) && isFile(normalized)) return path26.resolve(normalized);
11615
- const relative13 = normalized.replace(/^\/+/, "");
11903
+ if (path27.isAbsolute(normalized) && isFile(normalized)) return path27.resolve(normalized);
11904
+ const relative14 = normalized.replace(/^\/+/, "");
11616
11905
  for (const root of roots) {
11617
- const candidate = path26.resolve(root, relative13);
11906
+ const candidate = path27.resolve(root, relative14);
11618
11907
  if (isFile(candidate)) return candidate;
11619
11908
  }
11620
11909
  return null;
@@ -11630,10 +11919,10 @@ function viteBases(entries) {
11630
11919
  function fileForUrl(url, roots, bases) {
11631
11920
  let pathname;
11632
11921
  try {
11633
- const parsed = new URL(url);
11634
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:" && parsed.protocol !== "file:") return null;
11635
- pathname = parsed.pathname;
11636
- const base = bases.get(parsed.origin);
11922
+ const parsed2 = new URL(url);
11923
+ if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:" && parsed2.protocol !== "file:") return null;
11924
+ pathname = parsed2.pathname;
11925
+ const base = bases.get(parsed2.origin);
11637
11926
  if (base && pathname.startsWith(base)) pathname = pathname.slice(base.length - 1);
11638
11927
  } catch {
11639
11928
  return null;
@@ -11646,7 +11935,7 @@ function fileForUrl(url, roots, bases) {
11646
11935
  return file;
11647
11936
  }
11648
11937
  function relativeToRepo(file, repoRoot) {
11649
- return path26.relative(repoRoot, file).split(path26.sep).join("/");
11938
+ return path27.relative(repoRoot, file).split(path27.sep).join("/");
11650
11939
  }
11651
11940
  function scriptHash(entry) {
11652
11941
  return crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -11816,6 +12105,7 @@ function createSink() {
11816
12105
  flake: null,
11817
12106
  pickOffered: false,
11818
12107
  userPick: null,
12108
+ pause: null,
11819
12109
  locatorPages: new LocatorPageLog(),
11820
12110
  codeReach: null
11821
12111
  };
@@ -11832,11 +12122,11 @@ function configuredCodeReachRoots() {
11832
12122
  const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
11833
12123
  if (!raw) return null;
11834
12124
  try {
11835
- const parsed = JSON.parse(raw);
11836
- if (Array.isArray(parsed)) return parsed.filter((r) => typeof r === "string" && r !== "");
12125
+ const parsed2 = JSON.parse(raw);
12126
+ if (Array.isArray(parsed2)) return parsed2.filter((r) => typeof r === "string" && r !== "");
11837
12127
  } catch {
11838
12128
  }
11839
- return raw.split(path27.delimiter).filter(Boolean);
12129
+ return raw.split(path28.delimiter).filter(Boolean);
11840
12130
  }
11841
12131
  async function stopSinkCodeReach(sink, closing) {
11842
12132
  const session = sink.codeReach;
@@ -11851,7 +12141,7 @@ async function stopSinkCodeReach(sink, closing) {
11851
12141
  if (!entries) return;
11852
12142
  const info = sink.testInfo;
11853
12143
  const configFile = info?.config.configFile;
11854
- const configDir = configFile ? path27.dirname(configFile) : info?.config.rootDir ?? process.cwd();
12144
+ const configDir = configFile ? path28.dirname(configFile) : info?.config.rootDir ?? process.cwd();
11855
12145
  session.files = await resolveCodeReach(
11856
12146
  entries,
11857
12147
  codeReachRoots(configDir, configuredCodeReachRoots()),
@@ -12270,6 +12560,53 @@ function recordLocatorPage(sink, page, locator, location, arrival) {
12270
12560
  } catch {
12271
12561
  }
12272
12562
  }
12563
+ function pausesAt(pause, callerLocation) {
12564
+ return !pause.finished && (pause.next || pause.set.matches(callerLocation));
12565
+ }
12566
+ function locatorSource(target, method, args) {
12567
+ try {
12568
+ const text = String(target);
12569
+ if (text && !text.startsWith("[object")) return text;
12570
+ } catch {
12571
+ }
12572
+ return renderFailing({ method, args });
12573
+ }
12574
+ async function pauseBefore(sink, page, target, callerLocation, action, origin) {
12575
+ const pause = sink.pause;
12576
+ const testInfo = sink.testInfo;
12577
+ if (!pause || !testInfo || !callerLocation) return;
12578
+ pause.next = false;
12579
+ try {
12580
+ const locator = locatorSource(target, origin.method, origin.args);
12581
+ const onPick = async (picked) => {
12582
+ const pick = {
12583
+ failing: { method: origin.method, args: origin.args, rendered: locator, location: callerLocation },
12584
+ ...picked
12585
+ };
12586
+ applyPickToSnapshots(sink.capturedLocators, pick);
12587
+ sink.probedLocations.add(callerLocation);
12588
+ const place = callerLocation.slice(0, callerLocation.lastIndexOf(":"));
12589
+ console.log(`[piwi] Locator picked at ${place}: ${picked.picked.locator}`);
12590
+ await postPickToEditor(
12591
+ process.env[PIWI_LOCAL_DEBUG_ENV.editorSend],
12592
+ pickPayload(picked.picked.locator, callerLocation)
12593
+ );
12594
+ };
12595
+ const choice = await internalCall(
12596
+ page,
12597
+ () => pauseHere(
12598
+ page,
12599
+ testInfo,
12600
+ { location: callerLocation, action, locator, target },
12601
+ { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
12602
+ onPick
12603
+ )
12604
+ );
12605
+ if (choice === "step") pause.next = true;
12606
+ else if (choice === "finish") pause.finished = true;
12607
+ } catch {
12608
+ }
12609
+ }
12273
12610
  function wrapLocator(page, locator, originMethod, originArgs) {
12274
12611
  return new Proxy(locator, {
12275
12612
  get(target, prop) {
@@ -12291,6 +12628,12 @@ function wrapLocator(page, locator, originMethod, originArgs) {
12291
12628
  const isNot = Boolean(callArgs[1]?.isNot);
12292
12629
  const callerLocation = sink ? captureCallerLocation() : null;
12293
12630
  if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
12631
+ if (sink?.pause && pausesAt(sink.pause, callerLocation)) {
12632
+ await pauseBefore(sink, page, target, callerLocation, assertionLabel(expression, isNot), {
12633
+ method: originMethod,
12634
+ args: originArgs
12635
+ });
12636
+ }
12294
12637
  if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
12295
12638
  return callMethod(target, prop, callArgs);
12296
12639
  }
@@ -12329,6 +12672,9 @@ function wrapLocator(page, locator, originMethod, originArgs) {
12329
12672
  args: originArgs,
12330
12673
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
12331
12674
  };
12675
+ if (sink.pause && pausesAt(sink.pause, callerLocation)) {
12676
+ await pauseBefore(sink, page, target, callerLocation, String(prop), used);
12677
+ }
12332
12678
  const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
12333
12679
  let seq = -1;
12334
12680
  if (!alreadyProbed) {
@@ -12789,6 +13135,12 @@ var piwiFixtures = {
12789
13135
  if (conflict) throw new Error(conflict);
12790
13136
  const sink = createSink();
12791
13137
  sink.testInfo = testInfo;
13138
+ const pauseSet = currentPauseSet();
13139
+ if (pauseSet.size) {
13140
+ const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
13141
+ if (reason) logPauseSkipOnce(reason);
13142
+ else sink.pause = { set: pauseSet, next: false, finished: false };
13143
+ }
12792
13144
  if (isFlakeMode()) {
12793
13145
  const plan = loadFlakePlan();
12794
13146
  const role = flakeRoleForTest(plan, {
@@ -12868,12 +13220,12 @@ function extendPiwiFixtures(test) {
12868
13220
 
12869
13221
  // src/internal/ai/ai-fixtures.ts
12870
13222
  var fs24 = __toESM(require("fs"));
12871
- var path30 = __toESM(require("path"));
13223
+ var path31 = __toESM(require("path"));
12872
13224
  var import_test = require("@playwright/test");
12873
13225
 
12874
13226
  // src/internal/ai/artifact.ts
12875
13227
  var fs23 = __toESM(require("fs"));
12876
- var path28 = __toESM(require("path"));
13228
+ var path29 = __toESM(require("path"));
12877
13229
  var ARTIFACT_VERSION = 1;
12878
13230
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
12879
13231
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -12977,7 +13329,7 @@ function writeEntry(file, entry) {
12977
13329
  const canonical = serializeEntry(entry);
12978
13330
  return withEntryLock(file, () => {
12979
13331
  if (fs23.existsSync(file) && fs23.readFileSync(file, "utf8") === canonical) return { written: false };
12980
- fs23.mkdirSync(path28.dirname(file), { recursive: true });
13332
+ fs23.mkdirSync(path29.dirname(file), { recursive: true });
12981
13333
  const tmp = `${file}.${process.pid}.tmp`;
12982
13334
  fs23.writeFileSync(tmp, canonical);
12983
13335
  fs23.renameSync(tmp, file);
@@ -12986,7 +13338,7 @@ function writeEntry(file, entry) {
12986
13338
  }
12987
13339
  function withEntryLock(file, fn) {
12988
13340
  const lock = `${file}.lock`;
12989
- fs23.mkdirSync(path28.dirname(file), { recursive: true });
13341
+ fs23.mkdirSync(path29.dirname(file), { recursive: true });
12990
13342
  const deadline = Date.now() + 5e3;
12991
13343
  for (; ; ) {
12992
13344
  try {
@@ -13015,7 +13367,7 @@ function withEntryLock(file, fn) {
13015
13367
 
13016
13368
  // src/internal/ai/keys.ts
13017
13369
  var crypto4 = __toESM(require("crypto"));
13018
- var path29 = __toESM(require("path"));
13370
+ var path30 = __toESM(require("path"));
13019
13371
  var DEFAULT_AI_DIR = "__piwi__";
13020
13372
  function normalizeTemplate(template) {
13021
13373
  return template.trim().replace(/\s+/g, " ").toLowerCase();
@@ -13029,13 +13381,13 @@ function slug(text, maxLength = 40) {
13029
13381
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
13030
13382
  }
13031
13383
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
13032
- return path29.join(path29.dirname(specFile), dir, path29.basename(specFile));
13384
+ return path30.join(path30.dirname(specFile), dir, path30.basename(specFile));
13033
13385
  }
13034
13386
  function entryPath(params) {
13035
13387
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
13036
13388
  const hash = hashTemplate(basis, params.ordinal ?? 0);
13037
13389
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
13038
- return path29.join(entryDir(params.specFile, params.dir), name);
13390
+ return path30.join(entryDir(params.specFile, params.dir), name);
13039
13391
  }
13040
13392
  function parseLocation2(location) {
13041
13393
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -13215,9 +13567,9 @@ function renderSourceArg(arg) {
13215
13567
  if (Array.isArray(arg)) return `[${arg.map(renderSourceArg).join(", ")}]`;
13216
13568
  return `{ ${Object.entries(arg).map(([key, value]) => `${key}: ${renderSourceArg(value)}`).join(", ")} }`;
13217
13569
  }
13218
- function locatorSource(loc) {
13570
+ function locatorSource2(loc) {
13219
13571
  const head = `${loc.method}(${loc.args.map(renderSourceArg).join(", ")})`;
13220
- const tail = (loc.chain ?? []).map(locatorSource).join(".");
13572
+ const tail = (loc.chain ?? []).map(locatorSource2).join(".");
13221
13573
  return tail ? `${head}.${tail}` : head;
13222
13574
  }
13223
13575
  function describePostcondition(post) {
@@ -13348,6 +13700,97 @@ function compileFromCandidate(candidate) {
13348
13700
  return null;
13349
13701
  }
13350
13702
 
13703
+ // src/internal/ai/ground.ts
13704
+ var GROUNDED_METHODS = new Set(LOCATOR_METHODS.filter((method) => method !== "locator"));
13705
+ async function refLocator(page, ref) {
13706
+ if (!/^[a-z0-9]+$/i.test(ref)) return null;
13707
+ const getByRef = page.getByRef;
13708
+ try {
13709
+ const target = typeof getByRef === "function" ? getByRef.call(page, ref) : page.locator(`aria-ref=${ref}`);
13710
+ return await target.count() === 1 ? target : null;
13711
+ } catch {
13712
+ return null;
13713
+ }
13714
+ }
13715
+ async function matchesOnly(candidate, target) {
13716
+ try {
13717
+ return await candidate.count() === 1 && await candidate.and(target).count() === 1;
13718
+ } catch {
13719
+ return false;
13720
+ }
13721
+ }
13722
+ async function includes(candidate, target) {
13723
+ try {
13724
+ return await candidate.and(target).count() === 1;
13725
+ } catch {
13726
+ return false;
13727
+ }
13728
+ }
13729
+ function plainArg(arg, params, mask = true) {
13730
+ switch (arg.type) {
13731
+ case "string":
13732
+ return mask ? maskValues(arg.value, params) : arg.value;
13733
+ case "number":
13734
+ case "boolean":
13735
+ return arg.value;
13736
+ case "object": {
13737
+ const out = {};
13738
+ for (const [key, value] of arg.entries) {
13739
+ const plain = plainArg(value, params, mask);
13740
+ if (plain === void 0) return void 0;
13741
+ out[key] = plain;
13742
+ }
13743
+ return out;
13744
+ }
13745
+ default:
13746
+ return void 0;
13747
+ }
13748
+ }
13749
+ function chainToStructured(calls, params = {}) {
13750
+ const links = [];
13751
+ for (const call of calls) {
13752
+ if (!GROUNDED_METHODS.has(call.method)) return null;
13753
+ const args = [];
13754
+ for (const arg of call.args) {
13755
+ const plain = plainArg(arg, params, call.method !== "getByTestId");
13756
+ if (plain === void 0) return null;
13757
+ args.push(plain);
13758
+ }
13759
+ links.push({ method: call.method, args });
13760
+ }
13761
+ const [head, ...chain] = links;
13762
+ if (!head) return null;
13763
+ return chain.length > 0 ? { ...head, chain } : head;
13764
+ }
13765
+ async function normalizedLocator(target, params) {
13766
+ if (typeof target.normalize !== "function") return null;
13767
+ try {
13768
+ const chain = tryParseLocatorChain(String(await target.normalize()));
13769
+ return chain ? chainToStructured(chain.calls, params) : null;
13770
+ } catch {
13771
+ return null;
13772
+ }
13773
+ }
13774
+ async function groundElement(page, element, params) {
13775
+ const compiled = compileFromCandidate({
13776
+ role: element.role,
13777
+ name: element.name ?? null,
13778
+ level: element.level ?? null
13779
+ });
13780
+ const target = element.ref ? await refLocator(page, element.ref) : null;
13781
+ if (!target) return compiled;
13782
+ if (compiled) {
13783
+ const byName = buildLocator(page, compiled.locator, params);
13784
+ if (await matchesOnly(byName, target)) return compiled;
13785
+ if (!await includes(byName, target)) return compiled;
13786
+ }
13787
+ const normalized = await normalizedLocator(target, params);
13788
+ if (!normalized || !await matchesOnly(buildLocator(page, normalized, params), target)) return compiled;
13789
+ const fingerprint = compiled?.fingerprint ?? { role: element.role, name: element.name ?? null };
13790
+ if (!compiled && element.level != null) fingerprint.level = element.level;
13791
+ return { locator: normalized, fingerprint, score: compiled?.score ?? 0 };
13792
+ }
13793
+
13351
13794
  // src/internal/ai/resolution.ts
13352
13795
  var MAX_SNAPSHOT_CHARS = 24e3;
13353
13796
  var DEFAULT_MAX_STEPS = 20;
@@ -13368,12 +13811,6 @@ var ServerStepResolver = class {
13368
13811
  return await res.json();
13369
13812
  }
13370
13813
  };
13371
- function candidateOf(element) {
13372
- return { role: element.role, name: element.name ?? null, level: element.level ?? null };
13373
- }
13374
- function locatorFromElement(element) {
13375
- return compileFromCandidate(candidateOf(element));
13376
- }
13377
13814
  function lazyLocator(resolveOnce) {
13378
13815
  let pending = null;
13379
13816
  const ensure = () => pending ??= resolveOnce();
@@ -13385,20 +13822,21 @@ function lazyLocator(resolveOnce) {
13385
13822
  };
13386
13823
  return new Proxy({}, handler);
13387
13824
  }
13388
- function buildPostcondition(resolved, template) {
13825
+ async function buildPostcondition(resolved, template, ctx) {
13389
13826
  if (!resolved)
13390
13827
  throw new Error(`piwi AI: flow "${template}" resolved without a postcondition (the oracle is mandatory)`);
13391
13828
  if (resolved.assert === "url") {
13392
13829
  if (!resolved.url) throw new Error(`piwi AI: flow "${template}" postcondition is a url assert without a url`);
13393
13830
  return { assert: "url", url: resolved.url };
13394
13831
  }
13395
- const compiled = resolved.element ? locatorFromElement(resolved.element) : null;
13832
+ const compiled = resolved.element ? await groundElement(ctx.page, resolved.element, ctx.params) : null;
13396
13833
  if (!compiled) throw new Error(`piwi AI: flow "${template}" postcondition element could not be compiled`);
13397
13834
  return { assert: resolved.assert, locator: compiled.locator };
13398
13835
  }
13399
13836
  var readMaskedSnapshot = async (page, params) => {
13400
13837
  const raw = await ariaSnapshotBestEffort(page.locator("body"));
13401
- return raw ? maskValues(raw, params) : "";
13838
+ if (!raw) return "";
13839
+ return raw.split(/(\[ref=\w+\])/).map((part, i) => i % 2 === 1 ? part : maskValues(part, params)).join("");
13402
13840
  };
13403
13841
  var observeXhrResponses = async (page, action, settleMs) => {
13404
13842
  const urls = [];
@@ -13450,7 +13888,7 @@ async function resolveLocator(template, ctx) {
13450
13888
  ...screenshot ? { screenshot } : {}
13451
13889
  });
13452
13890
  if (!decision.element) throw new Error(`piwi AI: resolver returned no element for "${template}"`);
13453
- const compiled = locatorFromElement(decision.element);
13891
+ const compiled = await groundElement(ctx.page, decision.element, ctx.params);
13454
13892
  if (!compiled) throw new Error(`piwi AI: element for "${template}" could not be compiled to a stable locator`);
13455
13893
  const entry = {
13456
13894
  version: ARTIFACT_VERSION,
@@ -13483,14 +13921,14 @@ async function resolveRun(template, ctx) {
13483
13921
  ...screenshot ? { screenshot } : {}
13484
13922
  });
13485
13923
  if (decision.done) {
13486
- const postcondition = buildPostcondition(decision.postcondition, template);
13924
+ const postcondition = await buildPostcondition(decision.postcondition, template, ctx);
13487
13925
  await assertPostcondition(postcondition, { page: ctx.page, params: ctx.params });
13488
13926
  return { version: ARTIFACT_VERSION, kind: "run", template, steps, postcondition };
13489
13927
  }
13490
13928
  if (!decision.element || !decision.action) {
13491
13929
  throw new Error(`piwi AI: resolver returned neither a step nor done for "${template}"`);
13492
13930
  }
13493
- const compiled = locatorFromElement(decision.element);
13931
+ const compiled = await groundElement(ctx.page, decision.element, ctx.params);
13494
13932
  if (!compiled) throw new Error(`piwi AI: a step element for "${template}" could not be compiled`);
13495
13933
  const step = { locator: compiled.locator, action: decision.action, fingerprint: compiled.fingerprint };
13496
13934
  if (decision.value !== void 0) step.value = decision.value;
@@ -13549,18 +13987,18 @@ function testIdentity(testInfo) {
13549
13987
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
13550
13988
  }
13551
13989
  function relativeToCwd(file) {
13552
- return path30.relative(process.cwd(), file).split(path30.sep).join("/");
13990
+ return path31.relative(process.cwd(), file).split(path31.sep).join("/");
13553
13991
  }
13554
13992
  function recordIntents(intents, entry) {
13555
13993
  const add = (locator, kind) => {
13556
13994
  intents.set(`${entry.template}\0${locator}`, { template: entry.template, locator, kind });
13557
13995
  };
13558
13996
  if (entry.kind === "locator") {
13559
- add(locatorSource(entry.locator), "locator");
13997
+ add(locatorSource2(entry.locator), "locator");
13560
13998
  return;
13561
13999
  }
13562
- for (const step of entry.steps) add(locatorSource(step.locator), "run");
13563
- if (entry.postcondition.locator) add(locatorSource(entry.postcondition.locator), "run");
14000
+ for (const step of entry.steps) add(locatorSource2(step.locator), "run");
14001
+ if (entry.postcondition.locator) add(locatorSource2(entry.postcondition.locator), "run");
13564
14002
  }
13565
14003
  function missMessage(template, testTitle, relPath) {
13566
14004
  return [