@piwitests/reporter 0.47.0 → 0.49.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 };
@@ -91,7 +91,7 @@ var DEFAULTS = {
91
91
  defaultCapture: true,
92
92
  checkBaseUrl: false,
93
93
  streaming: true,
94
- streamingBatchSize: 5,
94
+ streamingBatchSize: 20,
95
95
  streamingBatchDelay: 2e3,
96
96
  maxStreamBufferBytes: 100 * 1024 * 1024,
97
97
  submitTimeout: 15 * 60 * 1e3,
@@ -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;
@@ -1865,6 +1869,20 @@ var StreamManager = class {
1865
1869
  this.heartbeatStopped = false;
1866
1870
  this.lastActivityAt = 0;
1867
1871
  this.heartbeatInterval = 15e3;
1872
+ /**
1873
+ * The heartbeat interval while nobody watches the run: during a long test no
1874
+ * other request goes out, so the heartbeat's answer is how the reporter learns
1875
+ * that someone opened the run.
1876
+ */
1877
+ this.unwatchedHeartbeatInterval = 5e3;
1878
+ /**
1879
+ * Whether anyone has the run open live on the dashboard, from the `watched`
1880
+ * flag of the last answer to an events or heartbeat request; `null` until the
1881
+ * dashboard says, and for a dashboard that does not. Step events are live
1882
+ * progress only (each `complete` event carries its test's steps), so none are
1883
+ * queued while it is `false`.
1884
+ */
1885
+ this.watched = null;
1868
1886
  /** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
1869
1887
  this.uploadedCaseFiles = /* @__PURE__ */ new WeakSet();
1870
1888
  /** Set when a test-result event left the live stream undelivered: too large to send, or still queued when the drain gave up. */
@@ -2035,8 +2053,13 @@ var StreamManager = class {
2035
2053
  this.pendingBeginEvents.enqueue(event);
2036
2054
  }
2037
2055
  }
2038
- /** Queue a test-case event. Triggers an immediate flush when the batch size is reached, otherwise schedules a timer-based flush. */
2056
+ /**
2057
+ * Queue a test-case event. Triggers an immediate flush when the batch size is
2058
+ * reached, otherwise schedules a timer-based flush. A step event is dropped
2059
+ * while nobody watches the run.
2060
+ */
2039
2061
  queueEvent(event) {
2062
+ if (this.watched === false && (event.type === "step-begin" || event.type === "step-end")) return;
2040
2063
  this.pendingEvents.enqueue(event);
2041
2064
  if (this.pendingEvents.length >= this.options.streamingBatchSize) {
2042
2065
  this.flush();
@@ -2064,11 +2087,12 @@ var StreamManager = class {
2064
2087
  while (chunks.length > 0) {
2065
2088
  const chunk = chunks.shift();
2066
2089
  try {
2067
- await this.httpClient.postJSON(
2090
+ const response = await this.httpClient.postJSON(
2068
2091
  `/api/test-runs/${this._runId}/events`,
2069
2092
  { streamToken: this._token, testCases: chunk },
2070
2093
  this._auth
2071
2094
  );
2095
+ this.noteWatched(response);
2072
2096
  this.retryCount = 0;
2073
2097
  this.lastActivityAt = Date.now();
2074
2098
  } catch (error) {
@@ -2142,13 +2166,29 @@ var StreamManager = class {
2142
2166
  if (this.pendingEvents.length > 0) this.flush();
2143
2167
  }, delay);
2144
2168
  }
2169
+ // Take the dashboard's answer on whether anyone watches the run. An answer
2170
+ // without the flag (an older dashboard) keeps step events flowing.
2171
+ noteWatched(response) {
2172
+ const watched = response?.watched;
2173
+ const next = typeof watched === "boolean" ? watched : null;
2174
+ if (next !== null && next !== this.watched) {
2175
+ this.logger.debug(
2176
+ next ? "The run is open on the dashboard: streaming live steps." : "Nobody is watching the run: live steps are not sent."
2177
+ );
2178
+ }
2179
+ this.watched = next;
2180
+ }
2181
+ // Idle time after which a heartbeat goes out.
2182
+ heartbeatDelay() {
2183
+ return this.watched === false ? this.unwatchedHeartbeatInterval : this.heartbeatInterval;
2184
+ }
2145
2185
  // Schedule the next idle heartbeat. Self-rescheduling; cleared by stopHeartbeat.
2146
2186
  scheduleHeartbeat() {
2147
2187
  if (this.heartbeatStopped || this.heartbeatTimer) return;
2148
2188
  this.heartbeatTimer = setTimeout(() => {
2149
2189
  this.heartbeatTimer = null;
2150
2190
  void this.sendHeartbeat();
2151
- }, this.heartbeatInterval);
2191
+ }, this.heartbeatDelay());
2152
2192
  }
2153
2193
  // Ping the server only when the run has actually been idle. Real event traffic
2154
2194
  // already bumps the server's activity timestamp, so a recent flush (or pending
@@ -2156,16 +2196,17 @@ var StreamManager = class {
2156
2196
  async sendHeartbeat() {
2157
2197
  if (this.heartbeatStopped || !this._enabled || !this._runId || !this._token) return;
2158
2198
  const idleFor = Date.now() - this.lastActivityAt;
2159
- if (idleFor < this.heartbeatInterval || this.pendingEvents.length > 0) {
2199
+ if (idleFor < this.heartbeatDelay() || this.pendingEvents.length > 0) {
2160
2200
  this.scheduleHeartbeat();
2161
2201
  return;
2162
2202
  }
2163
2203
  try {
2164
- await this.httpClient.postJSON(
2204
+ const response = await this.httpClient.postJSON(
2165
2205
  `/api/test-runs/${this._runId}/heartbeat`,
2166
2206
  { streamToken: this._token },
2167
2207
  this._auth
2168
2208
  );
2209
+ this.noteWatched(response);
2169
2210
  this.lastActivityAt = Date.now();
2170
2211
  } catch (error) {
2171
2212
  this.logger.debug(`Heartbeat failed: ${errorMessage(error)}`);
@@ -3743,9 +3784,9 @@ function loadAriaSampleSet(projectName) {
3743
3784
  let set = null;
3744
3785
  try {
3745
3786
  const raw = fs11.readFileSync(getAriaSampleFilePath(projectName), "utf8");
3746
- const parsed = JSON.parse(raw);
3747
- if (parsed.projectName === projectName && Array.isArray(parsed.identities)) {
3748
- 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"));
3749
3790
  }
3750
3791
  } catch {
3751
3792
  set = null;
@@ -3896,14 +3937,14 @@ var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
3896
3937
  var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
3897
3938
  var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
3898
3939
  function parseManifestJson(text) {
3899
- let parsed;
3940
+ let parsed2;
3900
3941
  try {
3901
- parsed = JSON.parse(text);
3942
+ parsed2 = JSON.parse(text);
3902
3943
  } catch {
3903
3944
  return null;
3904
3945
  }
3905
- if (!parsed || typeof parsed !== "object") return null;
3906
- const obj = parsed;
3946
+ if (!parsed2 || typeof parsed2 !== "object") return null;
3947
+ const obj = parsed2;
3907
3948
  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) => ({
3908
3949
  method: r.method,
3909
3950
  pattern: r.pattern,
@@ -4107,8 +4148,8 @@ function parseRouteKey(routeKey2) {
4107
4148
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
4108
4149
  }
4109
4150
  function patternToRegex(pattern) {
4110
- const path31 = pattern.split("?")[0] ?? pattern;
4111
- const escaped = path31.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
4151
+ const path32 = pattern.split("?")[0] ?? pattern;
4152
+ const escaped = path32.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
4112
4153
  return new RegExp(`^${escaped}$`);
4113
4154
  }
4114
4155
  function suitePathEqual(a, b) {
@@ -4131,13 +4172,13 @@ function matchProbeItem(plan, test) {
4131
4172
  function requestMatchesRoute(method, url, routeKey2) {
4132
4173
  const { method: rm, pattern } = parseRouteKey(routeKey2);
4133
4174
  if (rm && rm !== method.toUpperCase()) return false;
4134
- let path31;
4175
+ let path32;
4135
4176
  try {
4136
- path31 = new URL(url).pathname;
4177
+ path32 = new URL(url).pathname;
4137
4178
  } catch {
4138
- path31 = url.split("?")[0] ?? url;
4179
+ path32 = url.split("?")[0] ?? url;
4139
4180
  }
4140
- return patternToRegex(pattern).test(path31);
4181
+ return patternToRegex(pattern).test(path32);
4141
4182
  }
4142
4183
  function createProbeState() {
4143
4184
  return { navigated: false, matches: 0 };
@@ -4159,10 +4200,10 @@ function isProbeMode() {
4159
4200
  function loadPlan() {
4160
4201
  if (planCache !== void 0) return planCache;
4161
4202
  planCache = null;
4162
- const path31 = process.env[PIWI_PROBE_ENV.plan];
4163
- if (path31) {
4203
+ const path32 = process.env[PIWI_PROBE_ENV.plan];
4204
+ if (path32) {
4164
4205
  try {
4165
- planCache = JSON.parse(fs14.readFileSync(path31, "utf8"));
4206
+ planCache = JSON.parse(fs14.readFileSync(path32, "utf8"));
4166
4207
  } catch {
4167
4208
  planCache = null;
4168
4209
  }
@@ -4184,10 +4225,10 @@ function outcomeFromStatus(status, applied) {
4184
4225
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
4185
4226
  }
4186
4227
  function recordProbeOutcome(line) {
4187
- const path31 = process.env[PIWI_PROBE_ENV.results];
4188
- if (!path31) return;
4228
+ const path32 = process.env[PIWI_PROBE_ENV.results];
4229
+ if (!path32) return;
4189
4230
  try {
4190
- fs14.appendFileSync(path31, `${JSON.stringify(line)}
4231
+ fs14.appendFileSync(path32, `${JSON.stringify(line)}
4191
4232
  `);
4192
4233
  } catch {
4193
4234
  }
@@ -4410,8 +4451,8 @@ var FLAKE_LAB_RUN_METADATA_KEY = "piwiFlakeLab";
4410
4451
  // ../core/src/page-key.ts
4411
4452
  function normalizeRoute(url) {
4412
4453
  try {
4413
- const parsed = new URL(url);
4414
- let pathname = parsed.pathname;
4454
+ const parsed2 = new URL(url);
4455
+ let pathname = parsed2.pathname;
4415
4456
  pathname = pathname.replace(/\/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?=\/|$)/g, "/:jwt");
4416
4457
  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");
4417
4458
  pathname = pathname.replace(/\/[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}(?=\/|$)/gi, "/:ulid");
@@ -4419,8 +4460,8 @@ function normalizeRoute(url) {
4419
4460
  pathname = pathname.replace(/\/[0-9a-f]{32,}(?=\/|$)/gi, "/:token");
4420
4461
  pathname = pathname.replace(/\/\d+(?=\/|$)/g, "/:id");
4421
4462
  let queryString = "";
4422
- if (parsed.search) {
4423
- const params = new URLSearchParams(parsed.search);
4463
+ if (parsed2.search) {
4464
+ const params = new URLSearchParams(parsed2.search);
4424
4465
  const redacted = new URLSearchParams();
4425
4466
  for (const [key] of params) {
4426
4467
  redacted.set(key, "<redacted>");
@@ -4472,24 +4513,24 @@ function faultRouteAction(fault) {
4472
4513
  return fault === "slow" ? { kind: "delay", ms: SLOW_FAULT_DELAY_MS } : null;
4473
4514
  }
4474
4515
  function dropOneField(body) {
4475
- let parsed;
4516
+ let parsed2;
4476
4517
  try {
4477
- parsed = JSON.parse(body);
4518
+ parsed2 = JSON.parse(body);
4478
4519
  } catch {
4479
4520
  return body;
4480
4521
  }
4481
- if (Array.isArray(parsed)) {
4522
+ if (Array.isArray(parsed2)) {
4482
4523
  let key;
4483
- for (const el of parsed) {
4524
+ for (const el of parsed2) {
4484
4525
  if (el && typeof el === "object" && !Array.isArray(el)) {
4485
4526
  key ??= Object.keys(el)[0];
4486
4527
  if (key) delete el[key];
4487
4528
  }
4488
4529
  }
4489
- return JSON.stringify(parsed);
4530
+ return JSON.stringify(parsed2);
4490
4531
  }
4491
- if (parsed && typeof parsed === "object") {
4492
- const obj = parsed;
4532
+ if (parsed2 && typeof parsed2 === "object") {
4533
+ const obj = parsed2;
4493
4534
  const key = Object.keys(obj)[0];
4494
4535
  if (key) delete obj[key];
4495
4536
  return JSON.stringify(obj);
@@ -5297,8 +5338,8 @@ function ciBuildUrlFromMetadata(metadata) {
5297
5338
 
5298
5339
  // src/internal/submit/run-submitter.ts
5299
5340
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
5300
- function markSubmitFallback(run, path31, reason) {
5301
- run.metadata.ingestHealth = { submitFallback: { path: path31, reason } };
5341
+ function markSubmitFallback(run, path32, reason) {
5342
+ run.metadata.ingestHealth = { submitFallback: { path: path32, reason } };
5302
5343
  }
5303
5344
  var RunSubmitter = class {
5304
5345
  /**
@@ -7858,7 +7899,7 @@ function resourceTimeline(samples, censuses) {
7858
7899
  let startedAt = hasSamples ? samples.startedAt : Infinity;
7859
7900
  for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
7860
7901
  const shift = hasSamples ? samples.startedAt - startedAt : 0;
7861
- const relative13 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
7902
+ const relative14 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
7862
7903
  const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
7863
7904
  const perWorker = Math.max(
7864
7905
  60,
@@ -7866,17 +7907,17 @@ function resourceTimeline(samples, censuses) {
7866
7907
  );
7867
7908
  return {
7868
7909
  startedAt,
7869
- cpuPct: hasSamples ? downsamplePoints(relative13(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
7870
- memoryBytes: hasSamples ? downsamplePoints(relative13(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
7910
+ cpuPct: hasSamples ? downsamplePoints(relative14(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
7911
+ memoryBytes: hasSamples ? downsamplePoints(relative14(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
7871
7912
  pages: workers.map(({ worker, events }) => {
7872
- const points = relative13(events, -startedAt);
7913
+ const points = relative14(events, -startedAt);
7873
7914
  const last = points.pop();
7874
7915
  return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
7875
7916
  }),
7876
7917
  workers: trees.map((tree) => ({
7877
7918
  worker: tree.worker,
7878
- cpuCores: downsamplePoints(relative13(tree.cpuCores, shift), perWorker, "mean"),
7879
- memoryBytes: downsamplePoints(relative13(tree.memoryBytes, shift), perWorker, "max")
7919
+ cpuCores: downsamplePoints(relative14(tree.cpuCores, shift), perWorker, "mean"),
7920
+ memoryBytes: downsamplePoints(relative14(tree.memoryBytes, shift), perWorker, "max")
7880
7921
  }))
7881
7922
  };
7882
7923
  }
@@ -8039,8 +8080,8 @@ function formatTimeout(ms) {
8039
8080
  }
8040
8081
  function routeOf(url) {
8041
8082
  try {
8042
- const parsed = new URL(url);
8043
- return `${parsed.pathname}${parsed.search}` || "/";
8083
+ const parsed2 = new URL(url);
8084
+ return `${parsed2.pathname}${parsed2.search}` || "/";
8044
8085
  } catch {
8045
8086
  return url;
8046
8087
  }
@@ -8060,14 +8101,14 @@ function textOfGetByText(locator) {
8060
8101
  const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
8061
8102
  return m ? m[2] : null;
8062
8103
  }
8063
- function receivedDisplay(parsed) {
8064
- const received = parsed.received ?? "";
8065
- if (parsed.assertion !== "toHaveURL") return received;
8104
+ function receivedDisplay(parsed2) {
8105
+ const received = parsed2.received ?? "";
8106
+ if (parsed2.assertion !== "toHaveURL") return received;
8066
8107
  const m = /^"?(https?:\/\/[^"\s]+)"?$/.exec(received);
8067
8108
  return m ? `"${routeOf(m[1])}"` : received;
8068
8109
  }
8069
- function firstLineFallback(parsed) {
8070
- const line = (parsed.messageHead.split("\n")[0] ?? "").replace(/^Error:\s*/, "").replace(MASK_TOKEN_RE, "\u2026");
8110
+ function firstLineFallback(parsed2) {
8111
+ const line = (parsed2.messageHead.split("\n")[0] ?? "").replace(/^Error:\s*/, "").replace(MASK_TOKEN_RE, "\u2026");
8071
8112
  return truncateValue(line || "Unknown error", HEADLINE_MAX_CHARS);
8072
8113
  }
8073
8114
  var Line = class {
@@ -8104,148 +8145,148 @@ function timeoutSuffix(timeoutMs) {
8104
8145
  function timeoutParen(timeoutMs) {
8105
8146
  return timeoutMs !== null ? ` (${formatTimeout(timeoutMs)})` : "";
8106
8147
  }
8107
- function buildActionTimeout(parsed, opts) {
8148
+ function buildActionTimeout(parsed2, opts) {
8108
8149
  const line = new Line();
8109
- const verb = ACTION_VERBS[parsed.action ?? ""] ?? parsed.action ?? "action";
8110
- const state = STATE_PHRASES[parsed.lastState];
8111
- if (parsed.locator && state) {
8112
- return subject(line, parsed.locator, opts).text(` ${state} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed.timeoutMs));
8150
+ const verb = ACTION_VERBS[parsed2.action ?? ""] ?? parsed2.action ?? "action";
8151
+ const state = STATE_PHRASES[parsed2.lastState];
8152
+ if (parsed2.locator && state) {
8153
+ return subject(line, parsed2.locator, opts).text(` ${state} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
8113
8154
  }
8114
- if (parsed.locator && parsed.lastState === "resolved-count" && parsed.resolvedCount !== null) {
8115
- const count = parsed.resolvedCount;
8116
- return subject(line, parsed.locator, opts).text(` matched ${count === 0 ? "no" : count} ${countNoun(parsed.locator, count)} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed.timeoutMs));
8155
+ if (parsed2.locator && parsed2.lastState === "resolved-count" && parsed2.resolvedCount !== null) {
8156
+ const count = parsed2.resolvedCount;
8157
+ return subject(line, parsed2.locator, opts).text(` matched ${count === 0 ? "no" : count} ${countNoun(parsed2.locator, count)} \u2014 ${verb} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
8117
8158
  }
8118
- if (parsed.locator) {
8119
- return line.text(`${verb} on `).locator(opts.locator ?? parsed.locator).text(" timed out").text(timeoutSuffix(parsed.timeoutMs));
8159
+ if (parsed2.locator) {
8160
+ return line.text(`${verb} on `).locator(opts.locator ?? parsed2.locator).text(" timed out").text(timeoutSuffix(parsed2.timeoutMs));
8120
8161
  }
8121
- const call = parsed.subject && parsed.action ? `${parsed.subject}.${parsed.action}` : verb;
8122
- return line.text(`${call} timed out`).text(timeoutSuffix(parsed.timeoutMs));
8162
+ const call = parsed2.subject && parsed2.action ? `${parsed2.subject}.${parsed2.action}` : verb;
8163
+ return line.text(`${call} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
8123
8164
  }
8124
- function buildAssertion(parsed, opts) {
8165
+ function buildAssertion(parsed2, opts) {
8125
8166
  const line = new Line();
8126
- const matcher = parsed.assertion ?? "expect";
8127
- const notFound = parsed.lastState === "not-found" || parsed.lastState === "resolved-count" && parsed.resolvedCount === 0 || /element\(s\) not found/.test(parsed.received ?? "");
8128
- const expected = parsed.expected ? truncateValue(parsed.expected, opts.valueMax) : null;
8129
- const received = parsed.received ? truncateValue(receivedDisplay(parsed), opts.valueMax) : null;
8130
- if (matcher === "toHaveCount" && parsed.locator) {
8131
- const want = Number(parsed.expected);
8132
- const got = notFound ? 0 : Number(parsed.received);
8167
+ const matcher = parsed2.assertion ?? "expect";
8168
+ const notFound = parsed2.lastState === "not-found" || parsed2.lastState === "resolved-count" && parsed2.resolvedCount === 0 || /element\(s\) not found/.test(parsed2.received ?? "");
8169
+ const expected = parsed2.expected ? truncateValue(parsed2.expected, opts.valueMax) : null;
8170
+ const received = parsed2.received ? truncateValue(receivedDisplay(parsed2), opts.valueMax) : null;
8171
+ if (matcher === "toHaveCount" && parsed2.locator) {
8172
+ const want = Number(parsed2.expected);
8173
+ const got = notFound ? 0 : Number(parsed2.received);
8133
8174
  const wantText = Number.isFinite(want) ? String(want) : expected ?? "?";
8134
- const noun2 = countNoun(parsed.locator, Number.isFinite(want) ? want : 2);
8175
+ const noun2 = countNoun(parsed2.locator, Number.isFinite(want) ? want : 2);
8135
8176
  const gotText = Number.isFinite(got) ? got === 0 ? "none" : String(got) : received ?? "none";
8136
- return line.text(`Expected ${wantText} ${noun2}, found ${gotText} \u2014 `).locator(opts.locator ?? parsed.locator).text(" toHaveCount");
8177
+ return line.text(`Expected ${wantText} ${noun2}, found ${gotText} \u2014 `).locator(opts.locator ?? parsed2.locator).text(" toHaveCount");
8137
8178
  }
8138
8179
  const state = STATE_MATCHERS[matcher];
8139
8180
  if (state) {
8140
- const unmet = parsed.negated ? `stayed ${state.met}` : state.unmet;
8141
- if (!parsed.locator) return line.text(`Expected ${state.met}, page ${unmet}`).text(timeoutParen(parsed.timeoutMs));
8181
+ const unmet = parsed2.negated ? `stayed ${state.met}` : state.unmet;
8182
+ if (!parsed2.locator) return line.text(`Expected ${state.met}, page ${unmet}`).text(timeoutParen(parsed2.timeoutMs));
8142
8183
  if (notFound && matcher !== "toBeVisible" && matcher !== "toBeAttached") {
8143
- return subject(line, parsed.locator, opts).text(` was not found on the page \u2014 expected ${state.met}`).text(timeoutParen(parsed.timeoutMs));
8184
+ return subject(line, parsed2.locator, opts).text(` was not found on the page \u2014 expected ${state.met}`).text(timeoutParen(parsed2.timeoutMs));
8144
8185
  }
8145
- return subject(line, parsed.locator, opts).text(` ${unmet}`).text(timeoutParen(parsed.timeoutMs));
8186
+ return subject(line, parsed2.locator, opts).text(` ${unmet}`).text(timeoutParen(parsed2.timeoutMs));
8146
8187
  }
8147
8188
  const noun = VALUE_MATCHERS[matcher];
8148
8189
  if (noun) {
8149
- if (parsed.locator && notFound) {
8150
- return subject(line, parsed.locator, opts).text(" was not found on the page \u2014 expected ").text(`${noun} `).value(expected ?? "").text(timeoutParen(parsed.timeoutMs));
8190
+ if (parsed2.locator && notFound) {
8191
+ return subject(line, parsed2.locator, opts).text(" was not found on the page \u2014 expected ").text(`${noun} `).value(expected ?? "").text(timeoutParen(parsed2.timeoutMs));
8151
8192
  }
8152
8193
  line.text(`Expected ${noun} `);
8153
8194
  if (expected) line.value(expected);
8154
8195
  else line.text("to match");
8155
8196
  if (received) line.text(", got ").value(received);
8156
- if (parsed.locator)
8157
- line.text(" \u2014 ").locator(opts.locator ?? parsed.locator).text(` ${matcher}`);
8197
+ if (parsed2.locator)
8198
+ line.text(" \u2014 ").locator(opts.locator ?? parsed2.locator).text(` ${matcher}`);
8158
8199
  else if (matcher === "toHaveURL" || matcher === "toHaveTitle") line.text(` \u2014 page ${matcher}`);
8159
8200
  return line;
8160
8201
  }
8161
- if (matcher === "toPass") return line.text("expect.toPass never passed").text(timeoutParen(parsed.timeoutMs));
8202
+ if (matcher === "toPass") return line.text("expect.toPass never passed").text(timeoutParen(parsed2.timeoutMs));
8162
8203
  if (expected || received) {
8163
8204
  line.text("Expected ");
8164
8205
  if (expected) line.value(expected);
8165
8206
  else line.text("a different value");
8166
8207
  if (received) line.text(", got ").value(received);
8167
8208
  line.text(` \u2014 ${matcher}`);
8168
- if (parsed.locator) line.text(" on ").locator(opts.locator ?? parsed.locator);
8209
+ if (parsed2.locator) line.text(" on ").locator(opts.locator ?? parsed2.locator);
8169
8210
  return line;
8170
8211
  }
8171
- if (parsed.locator) {
8172
- return subject(line, parsed.locator, opts).text(` failed ${matcher}`).text(timeoutParen(parsed.timeoutMs));
8212
+ if (parsed2.locator) {
8213
+ return subject(line, parsed2.locator, opts).text(` failed ${matcher}`).text(timeoutParen(parsed2.timeoutMs));
8173
8214
  }
8174
8215
  return line.text(`${matcher} assertion failed`);
8175
8216
  }
8176
- function buildStrictMode(parsed, opts) {
8217
+ function buildStrictMode(parsed2, opts) {
8177
8218
  const line = new Line();
8178
- const count = parsed.resolvedCount ?? 0;
8179
- if (!parsed.locator) return line.text(`Locator matched ${count} elements \u2014 strict mode`);
8180
- return line.locator(opts.locator ?? parsed.locator).text(` matched ${count} ${countNoun(parsed.locator, count)} \u2014 strict mode`);
8219
+ const count = parsed2.resolvedCount ?? 0;
8220
+ if (!parsed2.locator) return line.text(`Locator matched ${count} elements \u2014 strict mode`);
8221
+ return line.locator(opts.locator ?? parsed2.locator).text(` matched ${count} ${countNoun(parsed2.locator, count)} \u2014 strict mode`);
8181
8222
  }
8182
- function buildNavigation(parsed, opts) {
8223
+ function buildNavigation(parsed2, opts) {
8183
8224
  const line = new Line();
8184
- const code = parsed.networkErrorCode?.replace(/^net::/, "") ?? null;
8225
+ const code = parsed2.networkErrorCode?.replace(/^net::/, "") ?? null;
8185
8226
  if (code) {
8186
8227
  const phrase = NETWORK_ERRORS[code] ?? `${code.replace(/^(?:ERR|NS_ERROR)_/, "").replace(/_/g, " ").toLowerCase().replace(/^\w/, (c) => c.toUpperCase())} loading`;
8187
8228
  line.text(phrase);
8188
- if (parsed.url) line.text(" ").value(truncateValue(parsed.url, opts.valueMax * 2));
8229
+ if (parsed2.url) line.text(" ").value(truncateValue(parsed2.url, opts.valueMax * 2));
8189
8230
  return line;
8190
8231
  }
8191
- const route = parsed.url ? routeOf(parsed.url) : null;
8192
- const timedOut = parsed.timeoutMs !== null || parsed.errorName === "TimeoutError";
8193
- if (parsed.action === "waitForURL" || parsed.action === "waitForNavigation") {
8232
+ const route = parsed2.url ? routeOf(parsed2.url) : null;
8233
+ const timedOut = parsed2.timeoutMs !== null || parsed2.errorName === "TimeoutError";
8234
+ if (parsed2.action === "waitForURL" || parsed2.action === "waitForNavigation") {
8194
8235
  line.text("Never navigated to ");
8195
8236
  if (route) line.value(truncateValue(route, opts.valueMax));
8196
8237
  else line.text("the expected URL");
8197
- return line.text(` \u2014 ${parsed.action} timed out`).text(timeoutSuffix(parsed.timeoutMs));
8238
+ return line.text(` \u2014 ${parsed2.action} timed out`).text(timeoutSuffix(parsed2.timeoutMs));
8198
8239
  }
8199
8240
  line.text("Navigation");
8200
8241
  if (route) line.text(" to ").value(truncateValue(route, opts.valueMax));
8201
- if (timedOut) return line.text(" timed out").text(timeoutSuffix(parsed.timeoutMs));
8242
+ if (timedOut) return line.text(" timed out").text(timeoutSuffix(parsed2.timeoutMs));
8202
8243
  return line.text(" failed");
8203
8244
  }
8204
- function buildTestTimeout(parsed, opts, ctx) {
8205
- const line = new Line().text("Test timed out").text(timeoutSuffix(parsed.timeoutMs));
8206
- if (parsed.timeoutPhase) {
8207
- const phase = parsed.timeoutPhase;
8245
+ function buildTestTimeout(parsed2, opts, ctx) {
8246
+ const line = new Line().text("Test timed out").text(timeoutSuffix(parsed2.timeoutMs));
8247
+ if (parsed2.timeoutPhase) {
8248
+ const phase = parsed2.timeoutPhase;
8208
8249
  const isHook = /^(?:before|after)(?:Each|All)$/.test(phase);
8209
8250
  return line.text(isHook ? ` in the "${phase}" hook` : ` while tearing down "${phase}"`);
8210
8251
  }
8211
- if (parsed.isNavigationFailure && parsed.url) {
8212
- return line.text(" while navigating to ").value(truncateValue(routeOf(parsed.url), opts.valueMax));
8252
+ if (parsed2.isNavigationFailure && parsed2.url) {
8253
+ return line.text(" while navigating to ").value(truncateValue(routeOf(parsed2.url), opts.valueMax));
8213
8254
  }
8214
- if (parsed.assertion && parsed.locator) {
8215
- const state = STATE_MATCHERS[parsed.assertion];
8255
+ if (parsed2.assertion && parsed2.locator) {
8256
+ const state = STATE_MATCHERS[parsed2.assertion];
8216
8257
  line.text(state ? ` while waiting for ` : " while expecting ");
8217
- subject(line, parsed.locator, opts);
8218
- return line.text(state ? ` to be ${state.met}` : ` ${parsed.assertion}`);
8258
+ subject(line, parsed2.locator, opts);
8259
+ return line.text(state ? ` to be ${state.met}` : ` ${parsed2.assertion}`);
8219
8260
  }
8220
- if (parsed.action && parsed.locator) {
8221
- const gerund = ACTION_GERUNDS[parsed.action] ?? `${ACTION_VERBS[parsed.action] ?? parsed.action} on`;
8261
+ if (parsed2.action && parsed2.locator) {
8262
+ const gerund = ACTION_GERUNDS[parsed2.action] ?? `${ACTION_VERBS[parsed2.action] ?? parsed2.action} on`;
8222
8263
  line.text(` while ${gerund} `);
8223
- return subject(line, parsed.locator, opts);
8264
+ return subject(line, parsed2.locator, opts);
8224
8265
  }
8225
- if (parsed.subject && parsed.action) return line.text(` during ${parsed.subject}.${parsed.action}`);
8266
+ if (parsed2.subject && parsed2.action) return line.text(` during ${parsed2.subject}.${parsed2.action}`);
8226
8267
  const step = ctx?.lastStepTitle?.trim();
8227
8268
  if (step) return line.text(" while ").value(`"${truncateValue(step, opts.valueMax * 1.5)}"`);
8228
8269
  return line;
8229
8270
  }
8230
- function buildCrash(parsed, opts) {
8271
+ function buildCrash(parsed2, opts) {
8231
8272
  const line = new Line();
8232
- const what = /Page crashed/i.test(parsed.messageHead) ? "Page crashed" : "Page or browser closed";
8233
- if (parsed.action) {
8234
- const verb = ACTION_VERBS[parsed.action] ?? parsed.action;
8273
+ const what = /Page crashed/i.test(parsed2.messageHead) ? "Page crashed" : "Page or browser closed";
8274
+ if (parsed2.action) {
8275
+ const verb = ACTION_VERBS[parsed2.action] ?? parsed2.action;
8235
8276
  line.text(`${what} during ${verb}`);
8236
- if (parsed.locator) line.text(" on ").locator(opts.locator ?? parsed.locator);
8277
+ if (parsed2.locator) line.text(" on ").locator(opts.locator ?? parsed2.locator);
8237
8278
  return line;
8238
8279
  }
8239
- if (parsed.assertion && parsed.locator) {
8240
- return line.text(`${what} while expecting `).locator(opts.locator ?? parsed.locator).text(` ${parsed.assertion}`);
8280
+ if (parsed2.assertion && parsed2.locator) {
8281
+ return line.text(`${what} while expecting `).locator(opts.locator ?? parsed2.locator).text(` ${parsed2.assertion}`);
8241
8282
  }
8242
8283
  return line.text(what);
8243
8284
  }
8244
8285
  var CUSTOM_MESSAGE_MAX_CHARS = 60;
8245
8286
  var SENTENCE_CASE_WORD_RE = /^[A-Z][a-z]*(?![A-Za-z])/;
8246
- function withLeads(line, parsed, ctx) {
8247
- const hook = ctx?.failedIn && !parsed.timeoutPhase ? ctx.failedIn.hook : null;
8248
- const message = parsed.customMessage ? truncateValue(parsed.customMessage, CUSTOM_MESSAGE_MAX_CHARS) : null;
8287
+ function withLeads(line, parsed2, ctx) {
8288
+ const hook = ctx?.failedIn && !parsed2.timeoutPhase ? ctx.failedIn.hook : null;
8289
+ const message = parsed2.customMessage ? truncateValue(parsed2.customMessage, CUSTOM_MESSAGE_MAX_CHARS) : null;
8249
8290
  if (!hook && !message) return line;
8250
8291
  const led = new Line();
8251
8292
  if (hook) led.text(`In ${hook}${message ? ", " : ": "}`);
@@ -8258,49 +8299,49 @@ function withLeads(line, parsed, ctx) {
8258
8299
  });
8259
8300
  return led;
8260
8301
  }
8261
- function build(parsed, opts, ctx) {
8262
- return withLeads(buildLine(parsed, opts, ctx), parsed, ctx);
8302
+ function build(parsed2, opts, ctx) {
8303
+ return withLeads(buildLine(parsed2, opts, ctx), parsed2, ctx);
8263
8304
  }
8264
- function buildLine(parsed, opts, ctx) {
8265
- switch (parsed.kind) {
8305
+ function buildLine(parsed2, opts, ctx) {
8306
+ switch (parsed2.kind) {
8266
8307
  case "action-timeout":
8267
- return buildActionTimeout(parsed, opts);
8308
+ return buildActionTimeout(parsed2, opts);
8268
8309
  case "assertion":
8269
8310
  case "assertion-timeout":
8270
- return buildAssertion(parsed, opts);
8311
+ return buildAssertion(parsed2, opts);
8271
8312
  case "strict-mode":
8272
- return buildStrictMode(parsed, opts);
8313
+ return buildStrictMode(parsed2, opts);
8273
8314
  case "navigation":
8274
- return buildNavigation(parsed, opts);
8315
+ return buildNavigation(parsed2, opts);
8275
8316
  case "test-timeout":
8276
- return buildTestTimeout(parsed, opts, ctx);
8317
+ return buildTestTimeout(parsed2, opts, ctx);
8277
8318
  case "crash":
8278
- return buildCrash(parsed, opts);
8319
+ return buildCrash(parsed2, opts);
8279
8320
  default:
8280
- return new Line().text(firstLineFallback(parsed));
8321
+ return new Line().text(firstLineFallback(parsed2));
8281
8322
  }
8282
8323
  }
8283
- function detailOf(parsed, headline) {
8284
- switch (parsed.kind) {
8324
+ function detailOf(parsed2, headline) {
8325
+ switch (parsed2.kind) {
8285
8326
  case "test-timeout":
8286
8327
  case "action-timeout": {
8287
- const state = parsed.lastStateLine;
8288
- if (!state || parsed.lastState === "not-found" || headline.includes(state)) return null;
8328
+ const state = parsed2.lastStateLine;
8329
+ if (!state || parsed2.lastState === "not-found" || headline.includes(state)) return null;
8289
8330
  return truncateValue(state, HEADLINE_MAX_CHARS);
8290
8331
  }
8291
8332
  case "assertion":
8292
8333
  case "assertion-timeout": {
8293
- const received = parsed.received;
8334
+ const received = parsed2.received;
8294
8335
  if (received && !/element\(s\) not found/.test(received)) {
8295
- const shown = truncateValue(receivedDisplay(parsed), SHORT_VALUE_MAX_CHARS).slice(0, 8);
8336
+ const shown = truncateValue(receivedDisplay(parsed2), SHORT_VALUE_MAX_CHARS).slice(0, 8);
8296
8337
  if (!headline.includes(shown)) return truncateValue(`Received: ${received}`, HEADLINE_MAX_CHARS);
8297
8338
  }
8298
- const state = parsed.lastStateLine;
8339
+ const state = parsed2.lastStateLine;
8299
8340
  if (!state || /^(?:waiting for |unexpected value )/.test(state)) return null;
8300
8341
  return truncateValue(state, HEADLINE_MAX_CHARS);
8301
8342
  }
8302
8343
  case "navigation":
8303
- return parsed.networkErrorCode ?? parsed.lastStateLine;
8344
+ return parsed2.networkErrorCode ?? parsed2.lastStateLine;
8304
8345
  case "strict-mode":
8305
8346
  case "crash":
8306
8347
  return null;
@@ -8308,16 +8349,16 @@ function detailOf(parsed, headline) {
8308
8349
  return null;
8309
8350
  }
8310
8351
  }
8311
- function describeFailure(parsed, ctx) {
8352
+ function describeFailure(parsed2, ctx) {
8312
8353
  const attempts = [
8313
- { locator: parsed.locator, valueMax: VALUE_MAX_CHARS },
8314
- { locator: parsed.leafLocator ?? parsed.locator, valueMax: VALUE_MAX_CHARS },
8315
- { locator: parsed.leafLocator ?? parsed.locator, valueMax: SHORT_VALUE_MAX_CHARS }
8354
+ { locator: parsed2.locator, valueMax: VALUE_MAX_CHARS },
8355
+ { locator: parsed2.leafLocator ?? parsed2.locator, valueMax: VALUE_MAX_CHARS },
8356
+ { locator: parsed2.leafLocator ?? parsed2.locator, valueMax: SHORT_VALUE_MAX_CHARS }
8316
8357
  ];
8317
- let line = build(parsed, attempts[0], ctx);
8358
+ let line = build(parsed2, attempts[0], ctx);
8318
8359
  for (const opts of attempts.slice(1)) {
8319
8360
  if (line.toString().length <= HEADLINE_MAX_CHARS) break;
8320
- line = build(parsed, opts, ctx);
8361
+ line = build(parsed2, opts, ctx);
8321
8362
  }
8322
8363
  let parts = line.parts.map((p) => ({ ...p, text: p.text.replace(MASK_TOKEN_RE, "\u2026") }));
8323
8364
  let headline = parts.map((p) => p.text).join("");
@@ -8327,10 +8368,10 @@ function describeFailure(parsed, ctx) {
8327
8368
  parts.push({ kind: "text", text: "\u2026" });
8328
8369
  }
8329
8370
  if (!headline.trim()) {
8330
- headline = firstLineFallback(parsed);
8371
+ headline = firstLineFallback(parsed2);
8331
8372
  parts = [{ kind: "text", text: headline }];
8332
8373
  }
8333
- return { headline, detail: detailOf(parsed, headline), parts };
8374
+ return { headline, detail: detailOf(parsed2, headline), parts };
8334
8375
  }
8335
8376
  function clipParts(parts, max) {
8336
8377
  const out = [];
@@ -9040,7 +9081,7 @@ async function resolveSelection(options = {}) {
9040
9081
  }
9041
9082
 
9042
9083
  // src/internal/capture/capture-fixtures.ts
9043
- var path27 = __toESM(require("path"));
9084
+ var path28 = __toESM(require("path"));
9044
9085
  var import_node_zlib2 = require("zlib");
9045
9086
 
9046
9087
  // ../picker-dom/src/probe.ts
@@ -10103,6 +10144,83 @@ function showPickerChoices(arg) {
10103
10144
  doc.body.appendChild(wrap);
10104
10145
  }
10105
10146
 
10147
+ // ../picker-dom/src/overlay-pause.ts
10148
+ function showPauseBar(arg) {
10149
+ const g = globalThis;
10150
+ const doc = g.document;
10151
+ delete g.__piwiPauseState;
10152
+ if (!doc || !doc.body) {
10153
+ g.__piwiPauseState = "resume";
10154
+ return;
10155
+ }
10156
+ if (typeof g.__piwiPauseCleanup === "function") g.__piwiPauseCleanup();
10157
+ const Z = 2147483600;
10158
+ const bar = doc.createElement("div");
10159
+ bar.id = "__piwi-pause-bar";
10160
+ bar.setAttribute("role", "toolbar");
10161
+ bar.setAttribute("aria-label", "Piwi: test paused");
10162
+ 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;`;
10163
+ const where = doc.createElement("div");
10164
+ where.style.cssText = "font-weight:600;white-space:nowrap;";
10165
+ where.textContent = `Paused at ${arg.place}${arg.attempt ? ` \xB7 attempt ${arg.attempt}` : ""}`;
10166
+ const what = doc.createElement("code");
10167
+ 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;";
10168
+ what.textContent = arg.locator ? `${arg.action} \xB7 ${arg.locator}` : arg.action;
10169
+ what.title = what.textContent;
10170
+ bar.appendChild(where);
10171
+ bar.appendChild(what);
10172
+ const done = (choice) => {
10173
+ cleanup();
10174
+ g.__piwiPauseState = choice;
10175
+ };
10176
+ const onKey = (e) => {
10177
+ if (e.key !== "Escape") return;
10178
+ e.preventDefault();
10179
+ e.stopImmediatePropagation();
10180
+ done("resume");
10181
+ };
10182
+ const cleanup = () => {
10183
+ doc.removeEventListener("keydown", onKey, true);
10184
+ bar.remove();
10185
+ delete g.__piwiPauseCleanup;
10186
+ };
10187
+ const buttons = [
10188
+ ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
10189
+ ["Step", "step", "Pause again at the next action", false],
10190
+ ["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
10191
+ ["Finish", "finish", "Run on and pause no more in this test", false]
10192
+ ];
10193
+ for (const [label, choice, hint, primary] of buttons) {
10194
+ const btn = doc.createElement("button");
10195
+ btn.type = "button";
10196
+ btn.textContent = label;
10197
+ btn.title = hint;
10198
+ 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;");
10199
+ btn.addEventListener("click", (e) => {
10200
+ e.preventDefault();
10201
+ e.stopImmediatePropagation();
10202
+ done(choice);
10203
+ });
10204
+ bar.appendChild(btn);
10205
+ }
10206
+ g.__piwiPauseCleanup = cleanup;
10207
+ doc.addEventListener("keydown", onKey, true);
10208
+ doc.body.appendChild(bar);
10209
+ }
10210
+ function pauseAnswered() {
10211
+ const g = globalThis;
10212
+ if (g.__piwiPauseState !== void 0) return true;
10213
+ return !g.document?.getElementById("__piwi-pause-bar");
10214
+ }
10215
+ function takePauseState() {
10216
+ const g = globalThis;
10217
+ const state = g.__piwiPauseState;
10218
+ if (typeof g.__piwiPauseCleanup === "function") g.__piwiPauseCleanup();
10219
+ delete g.__piwiPauseCleanup;
10220
+ delete g.__piwiPauseState;
10221
+ return typeof state === "string" ? state : null;
10222
+ }
10223
+
10106
10224
  // ../core/src/locator-generation.ts
10107
10225
  var TAG_TO_ROLE = {
10108
10226
  a: "link",
@@ -11139,11 +11257,11 @@ function deriveFailedLocator(testInfo) {
11139
11257
  ${err.stack ?? ""}`.replace(ANSI_RE2, "");
11140
11258
  const line = /^\s*Locator:\s*(.+)$/m.exec(text);
11141
11259
  if (!line) continue;
11142
- const parsed = parseLeafLocatorExpression(line[1].trim());
11143
- if (!parsed) continue;
11260
+ const parsed2 = parseLeafLocatorExpression(line[1].trim());
11261
+ if (!parsed2) continue;
11144
11262
  const loc = err.location;
11145
11263
  const location = loc ? `${path25.relative(process.cwd(), loc.file).split(path25.sep).join("/")}:${loc.line}:${loc.column}` : null;
11146
- return { method: parsed.method, args: parsed.args, location };
11264
+ return { method: parsed2.method, args: parsed2.args, location };
11147
11265
  }
11148
11266
  return null;
11149
11267
  }
@@ -11165,25 +11283,16 @@ async function cleanupPicker(page) {
11165
11283
  } catch {
11166
11284
  }
11167
11285
  }
11168
- async function runLocatorPicker(page, testInfo, failed, probe) {
11286
+ async function pickLocator(page, probe, failing) {
11169
11287
  try {
11170
- testInfo.setTimeout(0);
11171
- const rendered = failed ? renderFailing(failed) : null;
11172
- console.log(
11173
- `
11174
- [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).`
11175
- );
11176
- const overlayArg = { transport: "global", failing: rendered };
11288
+ const overlayArg = { transport: "global", failing };
11177
11289
  await page.evaluate(installPickerOverlay, overlayArg);
11178
11290
  await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
11179
11291
  timeout: 0,
11180
11292
  polling: 250
11181
11293
  });
11182
11294
  const state = await page.evaluate(() => globalThis.__piwiPickState);
11183
- if (state !== "picked") {
11184
- await cleanupPicker(page);
11185
- return null;
11186
- }
11295
+ if (state !== "picked") return null;
11187
11296
  const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
11188
11297
  const attrs = await handle.evaluate(probe.fn, probe.arg);
11189
11298
  await handle.dispose();
@@ -11221,11 +11330,10 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
11221
11330
  );
11222
11331
  if (ranked.length === 0) {
11223
11332
  console.log("[piwi] No stable locator could be generated for the picked element \u2014 nothing recorded.");
11224
- await cleanupPicker(page);
11225
11333
  return null;
11226
11334
  }
11227
11335
  await page.evaluate(showPickerChoices, {
11228
- failing: rendered,
11336
+ failing,
11229
11337
  choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
11230
11338
  });
11231
11339
  await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
@@ -11233,31 +11341,45 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
11233
11341
  polling: 250
11234
11342
  });
11235
11343
  const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
11236
- await cleanupPicker(page);
11237
11344
  if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
11238
11345
  const picked = { ...ranked[choice], pickedByUser: true };
11239
- const alternatives = [picked, ...ranked.filter((_, i) => i !== choice)];
11240
- const element = {
11241
- tagName: attrs.tagName,
11242
- attributes: attrs.attributes,
11243
- textContent: attrs.textContent,
11244
- accessibleName,
11245
- center: attrs.center,
11246
- ...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
11247
- ...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
11346
+ return {
11347
+ picked,
11348
+ alternatives: [picked, ...ranked.filter((_, i) => i !== choice)],
11349
+ element: {
11350
+ tagName: attrs.tagName,
11351
+ attributes: attrs.attributes,
11352
+ textContent: attrs.textContent,
11353
+ accessibleName,
11354
+ center: attrs.center,
11355
+ ...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
11356
+ ...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
11357
+ },
11358
+ ...anchors.length > 0 ? { anchors } : {}
11248
11359
  };
11360
+ } finally {
11361
+ await cleanupPicker(page);
11362
+ }
11363
+ }
11364
+ async function runLocatorPicker(page, testInfo, failed, probe) {
11365
+ try {
11366
+ testInfo.setTimeout(0);
11367
+ const rendered = failed ? renderFailing(failed) : null;
11368
+ console.log(
11369
+ `
11370
+ [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).`
11371
+ );
11372
+ const pick = await pickLocator(page, probe, rendered);
11373
+ if (!pick) return null;
11249
11374
  const location = failed?.location ?? null;
11250
11375
  console.log(
11251
11376
  failed ? `[piwi] Replacement picked for ${rendered}${location ? ` at ${location}` : ""}:
11252
- [piwi] ${picked.locator}` : `[piwi] Locator picked while inspecting:
11253
- [piwi] ${picked.locator}`
11377
+ [piwi] ${pick.picked.locator}` : `[piwi] Locator picked while inspecting:
11378
+ [piwi] ${pick.picked.locator}`
11254
11379
  );
11255
11380
  return {
11256
11381
  failing: failed ? { method: failed.method, args: failed.args, rendered, location } : null,
11257
- picked,
11258
- alternatives,
11259
- element,
11260
- ...anchors.length > 0 ? { anchors } : {}
11382
+ ...pick
11261
11383
  };
11262
11384
  } catch {
11263
11385
  return null;
@@ -11290,10 +11412,207 @@ function applyPickToSnapshots(snapshots, pick) {
11290
11412
  return true;
11291
11413
  }
11292
11414
 
11415
+ // src/internal/capture/pause-at.ts
11416
+ var path26 = __toESM(require("path"));
11417
+
11418
+ // ../core/src/editor-send.ts
11419
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
11420
+ function parsePairing(text) {
11421
+ const trimmed = text.trim();
11422
+ const hash = trimmed.indexOf("#");
11423
+ if (hash < 0) return null;
11424
+ const url = trimmed.slice(0, hash);
11425
+ const token = trimmed.slice(hash + 1);
11426
+ if (!/^[A-Za-z0-9_-]{16,}$/.test(token)) return null;
11427
+ let parsed2;
11428
+ try {
11429
+ parsed2 = new URL(url);
11430
+ } catch {
11431
+ return null;
11432
+ }
11433
+ if (parsed2.protocol !== "http:" || !LOOPBACK_HOSTS.has(parsed2.hostname) || !parsed2.port) return null;
11434
+ return { url: `${parsed2.origin}${parsed2.pathname}`, token };
11435
+ }
11436
+ function isInsidePath(file) {
11437
+ if (!file || file.length > 1e3 || file.includes("\0")) return false;
11438
+ if (file.startsWith("/") || file.startsWith("\\") || /^[A-Za-z]:/.test(file)) return false;
11439
+ return file.split(/[\\/]/).every((part) => part !== "..");
11440
+ }
11441
+
11442
+ // src/internal/capture/pause-at.ts
11443
+ var PauseSet = class {
11444
+ constructor(lines) {
11445
+ this.lines = lines;
11446
+ }
11447
+ get size() {
11448
+ return this.lines.size;
11449
+ }
11450
+ /** Whether a call site (`file:line:col`, as `captureCallerLocation` writes it) is on one of the lines. */
11451
+ matches(callerLocation) {
11452
+ if (!callerLocation) return false;
11453
+ const col = callerLocation.lastIndexOf(":");
11454
+ return col > 0 && this.lines.has(callerLocation.slice(0, col));
11455
+ }
11456
+ };
11457
+ var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
11458
+ function callSiteFile(file, cwd) {
11459
+ const slashed = file.replace(/\\/g, "/");
11460
+ const absolute = path26.isAbsolute(file) || path26.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
11461
+ let rel = absolute ? path26.relative(cwd, file) : path26.normalize(slashed);
11462
+ rel = rel.split(path26.sep).join("/").replace(/\\/g, "/");
11463
+ if (rel.startsWith("./")) rel = rel.slice(2);
11464
+ return rel;
11465
+ }
11466
+ var ignoredLogged = /* @__PURE__ */ new Set();
11467
+ function parsePauseAt(value, cwd = process.cwd()) {
11468
+ if (!value?.trim()) return EMPTY;
11469
+ const lines = /* @__PURE__ */ new Set();
11470
+ for (const raw of value.split(";")) {
11471
+ const entry = raw.trim();
11472
+ if (!entry) continue;
11473
+ const m = /^(.+):(\d+)$/.exec(entry);
11474
+ const line = m ? Number(m[2]) : 0;
11475
+ if (!m?.[1] || line < 1) {
11476
+ if (!ignoredLogged.has(entry)) {
11477
+ ignoredLogged.add(entry);
11478
+ console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt}: ignoring "${entry}": expected file:line`);
11479
+ }
11480
+ continue;
11481
+ }
11482
+ lines.add(`${callSiteFile(m[1], cwd)}:${line}`);
11483
+ }
11484
+ return lines.size ? new PauseSet(lines) : EMPTY;
11485
+ }
11486
+ var parsed = null;
11487
+ function currentPauseSet() {
11488
+ const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
11489
+ if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
11490
+ return parsed.set;
11491
+ }
11492
+ function pauseGateFromTestInfo(testInfo) {
11493
+ const use = testInfo.project?.use ?? {};
11494
+ return { ci: process.env.CI, headless: use.headless };
11495
+ }
11496
+ function pauseSkipReason(gate) {
11497
+ if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
11498
+ if (gate.headless !== false)
11499
+ return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
11500
+ return null;
11501
+ }
11502
+ function rearmedTimeout(before, pausedMs) {
11503
+ return before > 0 ? before + Math.max(0, Math.round(pausedMs)) : 0;
11504
+ }
11505
+ function pausePlace(callerLocation) {
11506
+ const col = callerLocation.lastIndexOf(":");
11507
+ const fileLine = col > 0 ? callerLocation.slice(0, col) : callerLocation;
11508
+ return fileLine.slice(fileLine.lastIndexOf("/") + 1);
11509
+ }
11510
+ function assertionLabel(expression, isNot) {
11511
+ const [first = "", ...rest] = expression.split(".").filter(Boolean);
11512
+ const name = first + rest.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
11513
+ return `${isNot ? "not." : ""}${name || "expect"}`;
11514
+ }
11515
+ function pickPlace(callerLocation) {
11516
+ const m = /^(.+):(\d+):\d+$/.exec(callerLocation);
11517
+ if (!m) return null;
11518
+ const file = m[1];
11519
+ if (!isInsidePath(file)) return null;
11520
+ return { file, line: Number(m[2]) };
11521
+ }
11522
+ function pickPayload(locator, callerLocation) {
11523
+ const at = pickPlace(callerLocation);
11524
+ return at ? { kind: "locator", text: locator, at } : { kind: "locator", text: locator };
11525
+ }
11526
+ var postFailureLogged = false;
11527
+ async function postPickToEditor(pairingAddress, payload, fetchImpl = fetch) {
11528
+ if (!pairingAddress) return false;
11529
+ const logOnce = (why) => {
11530
+ if (postFailureLogged) return;
11531
+ postFailureLogged = true;
11532
+ console.log(`[piwi] Could not send the picked locator to the editor: ${why}.`);
11533
+ };
11534
+ const pairing = parsePairing(pairingAddress);
11535
+ if (!pairing) {
11536
+ logOnce(`${PIWI_LOCAL_DEBUG_ENV.editorSend} is not a pairing address`);
11537
+ return false;
11538
+ }
11539
+ try {
11540
+ const response = await fetchImpl(pairing.url, {
11541
+ method: "POST",
11542
+ headers: { Authorization: `Bearer ${pairing.token}`, "Content-Type": "application/json" },
11543
+ body: JSON.stringify(payload),
11544
+ signal: AbortSignal.timeout(5e3)
11545
+ });
11546
+ if (response.ok) return true;
11547
+ logOnce(`the editor answered ${response.status}`);
11548
+ } catch (error) {
11549
+ logOnce(error instanceof Error ? error.message : String(error));
11550
+ }
11551
+ return false;
11552
+ }
11553
+ var skipLogged = false;
11554
+ function logPauseSkipOnce(reason) {
11555
+ if (skipLogged) return;
11556
+ skipLogged = true;
11557
+ console.log(`[piwi] ${PIWI_LOCAL_DEBUG_ENV.pauseAt} is set but breakpoints are skipped: ${reason}`);
11558
+ }
11559
+ var HIGHLIGHT_MS = 2e3;
11560
+ async function pauseHere(page, testInfo, point, probe, onPick) {
11561
+ const before = testInfo.timeout;
11562
+ const started = Date.now();
11563
+ let shown = null;
11564
+ try {
11565
+ testInfo.setTimeout(0);
11566
+ const highlight = point.target?.highlight;
11567
+ if (typeof highlight === "function") {
11568
+ let timer;
11569
+ shown = await Promise.race([
11570
+ Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
11571
+ new Promise((resolve7) => {
11572
+ timer = setTimeout(() => resolve7(null), HIGHLIGHT_MS);
11573
+ })
11574
+ ]);
11575
+ clearTimeout(timer);
11576
+ }
11577
+ const arg = {
11578
+ place: pausePlace(point.location),
11579
+ attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
11580
+ action: point.action,
11581
+ locator: point.locator
11582
+ };
11583
+ console.log(
11584
+ `[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).`
11585
+ );
11586
+ for (; ; ) {
11587
+ await page.evaluate(showPauseBar, arg);
11588
+ await page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 });
11589
+ const choice = await page.evaluate(takePauseState);
11590
+ if (choice === "resume" || choice === "step" || choice === "finish") return choice;
11591
+ if (choice !== "pick") continue;
11592
+ const picked = await pickLocator(page, probe, null).catch(() => null);
11593
+ if (picked) await onPick(picked);
11594
+ }
11595
+ } catch {
11596
+ return "resume";
11597
+ } finally {
11598
+ try {
11599
+ const dispose = shown?.dispose;
11600
+ const hide = page.hideHighlight;
11601
+ if (typeof dispose === "function") await dispose.call(shown);
11602
+ else if (typeof hide === "function") await hide.call(page);
11603
+ } catch {
11604
+ }
11605
+ try {
11606
+ testInfo.setTimeout(rearmedTimeout(before, Date.now() - started));
11607
+ } catch {
11608
+ }
11609
+ }
11610
+ }
11611
+
11293
11612
  // src/internal/capture/code-reach.ts
11294
11613
  var crypto3 = __toESM(require("crypto"));
11295
11614
  var fs22 = __toESM(require("fs"));
11296
- var path26 = __toESM(require("path"));
11615
+ var path27 = __toESM(require("path"));
11297
11616
  var import_node_child_process3 = require("child_process");
11298
11617
 
11299
11618
  // ../core/src/source-map.ts
@@ -11556,12 +11875,12 @@ function repositoryRoot(from) {
11556
11875
  }).trim();
11557
11876
  } catch {
11558
11877
  }
11559
- cachedRepoRoot = { from, root: path26.resolve(root) };
11878
+ cachedRepoRoot = { from, root: path27.resolve(root) };
11560
11879
  return cachedRepoRoot.root;
11561
11880
  }
11562
11881
  function codeReachRoots(configDir, configured) {
11563
11882
  const repoRoot = repositoryRoot(configDir);
11564
- const roots = configured?.length ? configured.map((r) => path26.resolve(configDir, r)) : [configDir];
11883
+ const roots = configured?.length ? configured.map((r) => path27.resolve(configDir, r)) : [configDir];
11565
11884
  return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
11566
11885
  }
11567
11886
  function isFile(file) {
@@ -11574,10 +11893,10 @@ function isFile(file) {
11574
11893
  function resolveSourcePath(source, roots) {
11575
11894
  const normalized = normalizeSourcePath(source);
11576
11895
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
11577
- if (path26.isAbsolute(normalized) && isFile(normalized)) return path26.resolve(normalized);
11578
- const relative13 = normalized.replace(/^\/+/, "");
11896
+ if (path27.isAbsolute(normalized) && isFile(normalized)) return path27.resolve(normalized);
11897
+ const relative14 = normalized.replace(/^\/+/, "");
11579
11898
  for (const root of roots) {
11580
- const candidate = path26.resolve(root, relative13);
11899
+ const candidate = path27.resolve(root, relative14);
11581
11900
  if (isFile(candidate)) return candidate;
11582
11901
  }
11583
11902
  return null;
@@ -11593,10 +11912,10 @@ function viteBases(entries) {
11593
11912
  function fileForUrl(url, roots, bases) {
11594
11913
  let pathname;
11595
11914
  try {
11596
- const parsed = new URL(url);
11597
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:" && parsed.protocol !== "file:") return null;
11598
- pathname = parsed.pathname;
11599
- const base = bases.get(parsed.origin);
11915
+ const parsed2 = new URL(url);
11916
+ if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:" && parsed2.protocol !== "file:") return null;
11917
+ pathname = parsed2.pathname;
11918
+ const base = bases.get(parsed2.origin);
11600
11919
  if (base && pathname.startsWith(base)) pathname = pathname.slice(base.length - 1);
11601
11920
  } catch {
11602
11921
  return null;
@@ -11609,7 +11928,7 @@ function fileForUrl(url, roots, bases) {
11609
11928
  return file;
11610
11929
  }
11611
11930
  function relativeToRepo(file, repoRoot) {
11612
- return path26.relative(repoRoot, file).split(path26.sep).join("/");
11931
+ return path27.relative(repoRoot, file).split(path27.sep).join("/");
11613
11932
  }
11614
11933
  function scriptHash(entry) {
11615
11934
  return crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -11779,6 +12098,7 @@ function createSink() {
11779
12098
  flake: null,
11780
12099
  pickOffered: false,
11781
12100
  userPick: null,
12101
+ pause: null,
11782
12102
  locatorPages: new LocatorPageLog(),
11783
12103
  codeReach: null
11784
12104
  };
@@ -11795,11 +12115,11 @@ function configuredCodeReachRoots() {
11795
12115
  const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
11796
12116
  if (!raw) return null;
11797
12117
  try {
11798
- const parsed = JSON.parse(raw);
11799
- if (Array.isArray(parsed)) return parsed.filter((r) => typeof r === "string" && r !== "");
12118
+ const parsed2 = JSON.parse(raw);
12119
+ if (Array.isArray(parsed2)) return parsed2.filter((r) => typeof r === "string" && r !== "");
11800
12120
  } catch {
11801
12121
  }
11802
- return raw.split(path27.delimiter).filter(Boolean);
12122
+ return raw.split(path28.delimiter).filter(Boolean);
11803
12123
  }
11804
12124
  async function stopSinkCodeReach(sink, closing) {
11805
12125
  const session = sink.codeReach;
@@ -11814,7 +12134,7 @@ async function stopSinkCodeReach(sink, closing) {
11814
12134
  if (!entries) return;
11815
12135
  const info = sink.testInfo;
11816
12136
  const configFile = info?.config.configFile;
11817
- const configDir = configFile ? path27.dirname(configFile) : info?.config.rootDir ?? process.cwd();
12137
+ const configDir = configFile ? path28.dirname(configFile) : info?.config.rootDir ?? process.cwd();
11818
12138
  session.files = await resolveCodeReach(
11819
12139
  entries,
11820
12140
  codeReachRoots(configDir, configuredCodeReachRoots()),
@@ -12233,6 +12553,53 @@ function recordLocatorPage(sink, page, locator, location, arrival) {
12233
12553
  } catch {
12234
12554
  }
12235
12555
  }
12556
+ function pausesAt(pause, callerLocation) {
12557
+ return !pause.finished && (pause.next || pause.set.matches(callerLocation));
12558
+ }
12559
+ function locatorSource(target, method, args) {
12560
+ try {
12561
+ const text = String(target);
12562
+ if (text && !text.startsWith("[object")) return text;
12563
+ } catch {
12564
+ }
12565
+ return renderFailing({ method, args });
12566
+ }
12567
+ async function pauseBefore(sink, page, target, callerLocation, action, origin) {
12568
+ const pause = sink.pause;
12569
+ const testInfo = sink.testInfo;
12570
+ if (!pause || !testInfo || !callerLocation) return;
12571
+ pause.next = false;
12572
+ try {
12573
+ const locator = locatorSource(target, origin.method, origin.args);
12574
+ const onPick = async (picked) => {
12575
+ const pick = {
12576
+ failing: { method: origin.method, args: origin.args, rendered: locator, location: callerLocation },
12577
+ ...picked
12578
+ };
12579
+ applyPickToSnapshots(sink.capturedLocators, pick);
12580
+ sink.probedLocations.add(callerLocation);
12581
+ const place = callerLocation.slice(0, callerLocation.lastIndexOf(":"));
12582
+ console.log(`[piwi] Locator picked at ${place}: ${picked.picked.locator}`);
12583
+ await postPickToEditor(
12584
+ process.env[PIWI_LOCAL_DEBUG_ENV.editorSend],
12585
+ pickPayload(picked.picked.locator, callerLocation)
12586
+ );
12587
+ };
12588
+ const choice = await internalCall(
12589
+ page,
12590
+ () => pauseHere(
12591
+ page,
12592
+ testInfo,
12593
+ { location: callerLocation, action, locator, target },
12594
+ { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
12595
+ onPick
12596
+ )
12597
+ );
12598
+ if (choice === "step") pause.next = true;
12599
+ else if (choice === "finish") pause.finished = true;
12600
+ } catch {
12601
+ }
12602
+ }
12236
12603
  function wrapLocator(page, locator, originMethod, originArgs) {
12237
12604
  return new Proxy(locator, {
12238
12605
  get(target, prop) {
@@ -12254,6 +12621,12 @@ function wrapLocator(page, locator, originMethod, originArgs) {
12254
12621
  const isNot = Boolean(callArgs[1]?.isNot);
12255
12622
  const callerLocation = sink ? captureCallerLocation() : null;
12256
12623
  if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
12624
+ if (sink?.pause && pausesAt(sink.pause, callerLocation)) {
12625
+ await pauseBefore(sink, page, target, callerLocation, assertionLabel(expression, isNot), {
12626
+ method: originMethod,
12627
+ args: originArgs
12628
+ });
12629
+ }
12257
12630
  if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
12258
12631
  return callMethod(target, prop, callArgs);
12259
12632
  }
@@ -12292,6 +12665,9 @@ function wrapLocator(page, locator, originMethod, originArgs) {
12292
12665
  args: originArgs,
12293
12666
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
12294
12667
  };
12668
+ if (sink.pause && pausesAt(sink.pause, callerLocation)) {
12669
+ await pauseBefore(sink, page, target, callerLocation, String(prop), used);
12670
+ }
12295
12671
  const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
12296
12672
  let seq = -1;
12297
12673
  if (!alreadyProbed) {
@@ -12752,6 +13128,12 @@ var piwiFixtures = {
12752
13128
  if (conflict) throw new Error(conflict);
12753
13129
  const sink = createSink();
12754
13130
  sink.testInfo = testInfo;
13131
+ const pauseSet = currentPauseSet();
13132
+ if (pauseSet.size) {
13133
+ const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
13134
+ if (reason) logPauseSkipOnce(reason);
13135
+ else sink.pause = { set: pauseSet, next: false, finished: false };
13136
+ }
12755
13137
  if (isFlakeMode()) {
12756
13138
  const plan = loadFlakePlan();
12757
13139
  const role = flakeRoleForTest(plan, {
@@ -12831,12 +13213,12 @@ function extendPiwiFixtures(test) {
12831
13213
 
12832
13214
  // src/internal/ai/ai-fixtures.ts
12833
13215
  var fs24 = __toESM(require("fs"));
12834
- var path30 = __toESM(require("path"));
13216
+ var path31 = __toESM(require("path"));
12835
13217
  var import_test = require("@playwright/test");
12836
13218
 
12837
13219
  // src/internal/ai/artifact.ts
12838
13220
  var fs23 = __toESM(require("fs"));
12839
- var path28 = __toESM(require("path"));
13221
+ var path29 = __toESM(require("path"));
12840
13222
  var ARTIFACT_VERSION = 1;
12841
13223
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
12842
13224
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -12940,7 +13322,7 @@ function writeEntry(file, entry) {
12940
13322
  const canonical = serializeEntry(entry);
12941
13323
  return withEntryLock(file, () => {
12942
13324
  if (fs23.existsSync(file) && fs23.readFileSync(file, "utf8") === canonical) return { written: false };
12943
- fs23.mkdirSync(path28.dirname(file), { recursive: true });
13325
+ fs23.mkdirSync(path29.dirname(file), { recursive: true });
12944
13326
  const tmp = `${file}.${process.pid}.tmp`;
12945
13327
  fs23.writeFileSync(tmp, canonical);
12946
13328
  fs23.renameSync(tmp, file);
@@ -12949,7 +13331,7 @@ function writeEntry(file, entry) {
12949
13331
  }
12950
13332
  function withEntryLock(file, fn) {
12951
13333
  const lock = `${file}.lock`;
12952
- fs23.mkdirSync(path28.dirname(file), { recursive: true });
13334
+ fs23.mkdirSync(path29.dirname(file), { recursive: true });
12953
13335
  const deadline = Date.now() + 5e3;
12954
13336
  for (; ; ) {
12955
13337
  try {
@@ -12978,7 +13360,7 @@ function withEntryLock(file, fn) {
12978
13360
 
12979
13361
  // src/internal/ai/keys.ts
12980
13362
  var crypto4 = __toESM(require("crypto"));
12981
- var path29 = __toESM(require("path"));
13363
+ var path30 = __toESM(require("path"));
12982
13364
  var DEFAULT_AI_DIR = "__piwi__";
12983
13365
  function normalizeTemplate(template) {
12984
13366
  return template.trim().replace(/\s+/g, " ").toLowerCase();
@@ -12992,13 +13374,13 @@ function slug(text, maxLength = 40) {
12992
13374
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
12993
13375
  }
12994
13376
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
12995
- return path29.join(path29.dirname(specFile), dir, path29.basename(specFile));
13377
+ return path30.join(path30.dirname(specFile), dir, path30.basename(specFile));
12996
13378
  }
12997
13379
  function entryPath(params) {
12998
13380
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
12999
13381
  const hash = hashTemplate(basis, params.ordinal ?? 0);
13000
13382
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
13001
- return path29.join(entryDir(params.specFile, params.dir), name);
13383
+ return path30.join(entryDir(params.specFile, params.dir), name);
13002
13384
  }
13003
13385
  function parseLocation2(location) {
13004
13386
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -13178,9 +13560,9 @@ function renderSourceArg(arg) {
13178
13560
  if (Array.isArray(arg)) return `[${arg.map(renderSourceArg).join(", ")}]`;
13179
13561
  return `{ ${Object.entries(arg).map(([key, value]) => `${key}: ${renderSourceArg(value)}`).join(", ")} }`;
13180
13562
  }
13181
- function locatorSource(loc) {
13563
+ function locatorSource2(loc) {
13182
13564
  const head = `${loc.method}(${loc.args.map(renderSourceArg).join(", ")})`;
13183
- const tail = (loc.chain ?? []).map(locatorSource).join(".");
13565
+ const tail = (loc.chain ?? []).map(locatorSource2).join(".");
13184
13566
  return tail ? `${head}.${tail}` : head;
13185
13567
  }
13186
13568
  function describePostcondition(post) {
@@ -13512,18 +13894,18 @@ function testIdentity(testInfo) {
13512
13894
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
13513
13895
  }
13514
13896
  function relativeToCwd(file) {
13515
- return path30.relative(process.cwd(), file).split(path30.sep).join("/");
13897
+ return path31.relative(process.cwd(), file).split(path31.sep).join("/");
13516
13898
  }
13517
13899
  function recordIntents(intents, entry) {
13518
13900
  const add = (locator, kind) => {
13519
13901
  intents.set(`${entry.template}\0${locator}`, { template: entry.template, locator, kind });
13520
13902
  };
13521
13903
  if (entry.kind === "locator") {
13522
- add(locatorSource(entry.locator), "locator");
13904
+ add(locatorSource2(entry.locator), "locator");
13523
13905
  return;
13524
13906
  }
13525
- for (const step of entry.steps) add(locatorSource(step.locator), "run");
13526
- if (entry.postcondition.locator) add(locatorSource(entry.postcondition.locator), "run");
13907
+ for (const step of entry.steps) add(locatorSource2(step.locator), "run");
13908
+ if (entry.postcondition.locator) add(locatorSource2(entry.postcondition.locator), "run");
13527
13909
  }
13528
13910
  function missMessage(template, testTitle, relPath) {
13529
13911
  return [