@piwitests/reporter 0.39.0 → 0.40.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
@@ -568,6 +568,7 @@ function toWireTestCase(tc) {
568
568
  locks: rest.locks ?? null,
569
569
  testMeta: rest.testMeta ?? null,
570
570
  locatorSnapshots: rest.locatorSnapshots || null,
571
+ locatorPages: rest.locatorPages || null,
571
572
  didNotRunReason: rest.didNotRunReason ?? null,
572
573
  blockedBy: rest.blockedBy ?? null
573
574
  };
@@ -996,6 +997,7 @@ var ATTACHMENT_NAMES = {
996
997
  dialogs: "piwi-dialogs",
997
998
  network: "piwi-network",
998
999
  pageInventory: "piwi-page-inventory",
1000
+ locatorPages: "piwi-locator-pages",
999
1001
  webVitals: "piwi-web-vitals",
1000
1002
  locatorSuggestion: "piwi-locator-suggestion",
1001
1003
  pageState: "piwi-page-state",
@@ -2146,11 +2148,14 @@ var MAX_STEP_PARAM_VALUE_CHARS = 200;
2146
2148
  function capStepValue(value, max) {
2147
2149
  return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}\u2026` : value;
2148
2150
  }
2151
+ var HTTP_METHOD_TITLE_RE = /^(?:get|post|put|patch|delete|head|options|fetch)$/i;
2149
2152
  function categorizeStep(title, pwCategory, params) {
2150
2153
  if (!title) return "other";
2151
- if (pwCategory === "hook" || pwCategory === "fixture") return pwCategory;
2154
+ if (pwCategory === "hook" || pwCategory === "fixture" || pwCategory === "test.step") return pwCategory;
2152
2155
  if (pwCategory === "expect") return "assertion";
2156
+ if (pwCategory === "test.attach") return "attach";
2153
2157
  const lower = title.toLowerCase();
2158
+ if (HTTP_METHOD_TITLE_RE.test(title.trim())) return "api";
2154
2159
  if (lower.startsWith("wait for") || lower.startsWith("locator.waitfor") || lower.startsWith("page.waitfor") || lower.startsWith("frame.waitfor"))
2155
2160
  return "wait";
2156
2161
  if (typeof params?.url === "string" && pwCategory !== "expect") return "navigation";
@@ -2188,13 +2193,14 @@ function normalizeStepParams(raw) {
2188
2193
  }
2189
2194
  return count > 0 ? out : void 0;
2190
2195
  }
2191
- function flattenSteps(steps) {
2196
+ function flattenSteps(steps, depth = 0) {
2192
2197
  const result = [];
2193
2198
  for (const step of steps) {
2194
2199
  const flat = {
2195
2200
  title: step.title,
2196
2201
  duration: step.duration,
2197
- category: categorizeStep(step.title, step.category, step.params)
2202
+ category: categorizeStep(step.title, step.category, step.params),
2203
+ depth
2198
2204
  };
2199
2205
  if (typeof step.subtitle === "string" && step.subtitle.length > 0) flat.subtitle = maskTokenLike(step.subtitle);
2200
2206
  const params = normalizeStepParams(step.params);
@@ -2206,7 +2212,7 @@ function flattenSteps(steps) {
2206
2212
  if (step.location) flat.location = `${step.location.file}:${step.location.line}:${step.location.column}`;
2207
2213
  if (step.startTime) flat.startTime = step.startTime instanceof Date ? step.startTime.getTime() : step.startTime;
2208
2214
  result.push(flat);
2209
- if (step.steps?.length > 0) result.push(...flattenSteps(step.steps));
2215
+ if (step.steps?.length > 0) result.push(...flattenSteps(step.steps, depth + 1));
2210
2216
  }
2211
2217
  return result;
2212
2218
  }
@@ -2318,7 +2324,8 @@ function findOwnPackageJson(fromDir) {
2318
2324
  const pkgPath = path8.resolve(fromDir, offset, "package.json");
2319
2325
  const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
2320
2326
  const { name, version } = pkg;
2321
- if (name === "@piwitests/reporter" && typeof version === "string") return { version };
2327
+ if (name === "@piwitests/reporter" && typeof version === "string")
2328
+ return { version, root: path8.dirname(pkgPath) };
2322
2329
  } catch {
2323
2330
  }
2324
2331
  }
@@ -2772,8 +2779,8 @@ function parseRouteKey(routeKey) {
2772
2779
  return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
2773
2780
  }
2774
2781
  function patternToRegex(pattern) {
2775
- const path23 = pattern.split("?")[0] ?? pattern;
2776
- const escaped = path23.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2782
+ const path24 = pattern.split("?")[0] ?? pattern;
2783
+ const escaped = path24.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2777
2784
  return new RegExp(`^${escaped}$`);
2778
2785
  }
2779
2786
  function suitePathEqual(a, b) {
@@ -2796,13 +2803,13 @@ function matchProbeItem(plan, test) {
2796
2803
  function requestMatchesRoute(method, url, routeKey) {
2797
2804
  const { method: rm, pattern } = parseRouteKey(routeKey);
2798
2805
  if (rm && rm !== method.toUpperCase()) return false;
2799
- let path23;
2806
+ let path24;
2800
2807
  try {
2801
- path23 = new URL(url).pathname;
2808
+ path24 = new URL(url).pathname;
2802
2809
  } catch {
2803
- path23 = url.split("?")[0] ?? url;
2810
+ path24 = url.split("?")[0] ?? url;
2804
2811
  }
2805
- return patternToRegex(pattern).test(path23);
2812
+ return patternToRegex(pattern).test(path24);
2806
2813
  }
2807
2814
  function createProbeState() {
2808
2815
  return { navigated: false, matches: 0 };
@@ -2824,10 +2831,10 @@ function isProbeMode() {
2824
2831
  function loadPlan() {
2825
2832
  if (planCache !== void 0) return planCache;
2826
2833
  planCache = null;
2827
- const path23 = process.env[PIWI_PROBE_ENV.plan];
2828
- if (path23) {
2834
+ const path24 = process.env[PIWI_PROBE_ENV.plan];
2835
+ if (path24) {
2829
2836
  try {
2830
- planCache = JSON.parse(fs14.readFileSync(path23, "utf8"));
2837
+ planCache = JSON.parse(fs14.readFileSync(path24, "utf8"));
2831
2838
  } catch {
2832
2839
  planCache = null;
2833
2840
  }
@@ -2849,10 +2856,10 @@ function outcomeFromStatus(status, applied) {
2849
2856
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2850
2857
  }
2851
2858
  function recordProbeOutcome(line) {
2852
- const path23 = process.env[PIWI_PROBE_ENV.results];
2853
- if (!path23) return;
2859
+ const path24 = process.env[PIWI_PROBE_ENV.results];
2860
+ if (!path24) return;
2854
2861
  try {
2855
- fs14.appendFileSync(path23, `${JSON.stringify(line)}
2862
+ fs14.appendFileSync(path24, `${JSON.stringify(line)}
2856
2863
  `);
2857
2864
  } catch {
2858
2865
  }
@@ -4089,12 +4096,20 @@ function readUrl(text, callLog, received) {
4089
4096
  }
4090
4097
  return null;
4091
4098
  }
4099
+ var MATCHER_LINE_RE = /^(?:expect[.(]|Timed out\b|Expected\b|Received\b|Locator\b)/;
4100
+ function readCustomMessage(messageHead) {
4101
+ const lines = messageHead.split("\n").map((line) => line.trim()).filter(Boolean);
4102
+ if (lines.length < 2) return null;
4103
+ const first = lines[0].replace(/^Error:\s*/, "");
4104
+ if (!first || MATCHER_LINE_RE.test(first)) return null;
4105
+ return /^(?:expect[.(]|Timed out\b)/.test(lines[1]) ? first : null;
4106
+ }
4092
4107
  function parsePlaywrightError(raw, context) {
4093
4108
  const clean = stripAnsi(raw ?? "").replace(/\r\n?/g, "\n");
4094
4109
  const text = withoutStackFrames(clean);
4095
4110
  const messageHead = extractMessageHead(clean);
4096
- const firstLine = messageHead.split("\n")[0] ?? "";
4097
- const errorName = ERROR_NAME_RE.exec(firstLine)?.[1] ?? null;
4111
+ const firstLine2 = messageHead.split("\n")[0] ?? "";
4112
+ const errorName = ERROR_NAME_RE.exec(firstLine2)?.[1] ?? null;
4098
4113
  const strict = STRICT_RE.exec(text);
4099
4114
  const call = CALL_RE.exec(text);
4100
4115
  const callLog = readCallLog(text);
@@ -4164,12 +4179,134 @@ function parsePlaywrightError(raw, context) {
4164
4179
  lastCallLogLine: callLog.lastLine,
4165
4180
  lastStateLine: callLog.stateLine,
4166
4181
  messageHead,
4182
+ customMessage: isAssertion ? readCustomMessage(messageHead) : null,
4167
4183
  topFrame: frame ? `${frame.file}:${frame.line}` : null,
4168
4184
  isNavigationFailure,
4169
4185
  isLocatorResolutionFailure
4170
4186
  };
4171
4187
  }
4172
4188
 
4189
+ // ../core/src/step-tree.ts
4190
+ var SPAN_TOLERANCE_MS = 1;
4191
+ var SETUP_CONTAINER_RE = /^before hooks$/i;
4192
+ var TEARDOWN_CONTAINER_RE = /^(?:after hooks|worker cleanup|worker teardown)$/i;
4193
+ var NAMED_HOOK_RE = /^(before|after)(each|all) hook$/i;
4194
+ var FIXTURE_TITLE_RE = /^fixture\s+["“](.+)["”]$/i;
4195
+ var CAPTURE_ATTACH_RE = /^attach\s+["“]piwi-/i;
4196
+ var CAPTURE_FIXTURE_RE = /^fixture\s+["“]piwiCapture["”]$/i;
4197
+ var CAPTURE_PROBE_TITLE_RE = /^(?:evaluate|aria snapshot|locator\.(?:evaluate|ariasnapshot)|page\.evaluate)\b/i;
4198
+ var REPORTER_BUNDLE_RE = /[\\/](?:@piwitests[\\/]reporter|packages[\\/]reporter)[\\/]dist[\\/]/;
4199
+ function str(value) {
4200
+ return typeof value === "string" ? value : "";
4201
+ }
4202
+ function num(value) {
4203
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
4204
+ }
4205
+ function errorMessage2(step) {
4206
+ const error = step.error;
4207
+ if (typeof error === "string") return error;
4208
+ return str(error?.message);
4209
+ }
4210
+ function isFailedStep(step) {
4211
+ return step.failed === true || errorMessage2(step).trim().length > 0;
4212
+ }
4213
+ function isCaptureStep(step) {
4214
+ const title = str(step.title).trim();
4215
+ if (CAPTURE_ATTACH_RE.test(title) || CAPTURE_FIXTURE_RE.test(title)) return true;
4216
+ return CAPTURE_PROBE_TITLE_RE.test(title) && REPORTER_BUNDLE_RE.test(str(step.location));
4217
+ }
4218
+ function isPhaseContainer(step) {
4219
+ const title = str(step.title).trim();
4220
+ return SETUP_CONTAINER_RE.test(title) || TEARDOWN_CONTAINER_RE.test(title);
4221
+ }
4222
+ function stepParents(steps) {
4223
+ const parents = steps.map(() => -1);
4224
+ if (steps.length === 0) return parents;
4225
+ if (steps.every((s) => num(s.depth) !== null)) {
4226
+ const stack = [];
4227
+ steps.forEach((step, i) => {
4228
+ const depth = num(step.depth);
4229
+ while (stack.length > 0 && num(steps[stack[stack.length - 1]].depth) >= depth) stack.pop();
4230
+ parents[i] = stack.length > 0 ? stack[stack.length - 1] : -1;
4231
+ stack.push(i);
4232
+ });
4233
+ return parents;
4234
+ }
4235
+ if (steps.every((s) => num(s.startTime) !== null && num(s.duration) !== null)) {
4236
+ const start = (i) => num(steps[i].startTime);
4237
+ const end = (i) => start(i) + Math.max(0, num(steps[i].duration));
4238
+ const contains = (a, b) => end(a) > start(a) && start(b) >= start(a) - SPAN_TOLERANCE_MS && start(b) < end(a) && end(b) <= end(a) + SPAN_TOLERANCE_MS;
4239
+ const stack = [];
4240
+ steps.forEach((_, i) => {
4241
+ while (stack.length > 0 && !contains(stack[stack.length - 1], i)) stack.pop();
4242
+ parents[i] = stack.length > 0 ? stack[stack.length - 1] : -1;
4243
+ stack.push(i);
4244
+ });
4245
+ }
4246
+ return parents;
4247
+ }
4248
+ function isDescendant(descendant, ancestor, parents) {
4249
+ for (let p = parents[descendant] ?? -1; p !== -1; p = parents[p] ?? -1) {
4250
+ if (p === ancestor) return true;
4251
+ }
4252
+ return false;
4253
+ }
4254
+ function topAncestor(index, parents) {
4255
+ let top = index;
4256
+ while ((parents[top] ?? -1) !== -1) top = parents[top];
4257
+ return top;
4258
+ }
4259
+ function stepPhases(steps, parents = stepParents(steps)) {
4260
+ return steps.map((_, i) => {
4261
+ const top = str(steps[topAncestor(i, parents)].title).trim();
4262
+ if (SETUP_CONTAINER_RE.test(top)) return "setup";
4263
+ if (TEARDOWN_CONTAINER_RE.test(top)) return "teardown";
4264
+ return "body";
4265
+ });
4266
+ }
4267
+ function failingStepIndex(steps, parents = stepParents(steps)) {
4268
+ const candidates = [];
4269
+ steps.forEach((step, i) => {
4270
+ if (isFailedStep(step) && !isCaptureStep(step)) candidates.push(i);
4271
+ });
4272
+ if (candidates.length === 0) return null;
4273
+ let current = candidates[0];
4274
+ for (; ; ) {
4275
+ const inner = candidates.find((c) => c > current && isDescendant(c, current, parents));
4276
+ if (inner === void 0) return current;
4277
+ current = inner;
4278
+ }
4279
+ }
4280
+ function stepHookName(step) {
4281
+ const title = str(step.title).trim();
4282
+ const category = str(step.category);
4283
+ const named = NAMED_HOOK_RE.exec(title);
4284
+ if (named) return `${named[1].toLowerCase()}${named[2][0].toUpperCase()}${named[2].slice(1).toLowerCase()}`;
4285
+ const fixture = FIXTURE_TITLE_RE.exec(title);
4286
+ if (fixture) return `fixture "${fixture[1]}"`;
4287
+ if (category === "fixture") return title ? `fixture "${title.replace(/^fixture:\s*/i, "")}"` : null;
4288
+ if (category === "hook" && title && !isPhaseContainer(step)) return `hook "${title}"`;
4289
+ return null;
4290
+ }
4291
+ function firstLine(message) {
4292
+ return stripAnsi(str(message)).split("\n")[0].trim();
4293
+ }
4294
+ function failureHookContext(steps, error, parents = stepParents(steps)) {
4295
+ const index = failingStepIndex(steps, parents);
4296
+ if (index === null) return null;
4297
+ const phase = stepPhases(steps, parents)[index];
4298
+ if (phase === "body") return null;
4299
+ if (error != null) {
4300
+ const stepError = firstLine(errorMessage2(steps[index]));
4301
+ if (stepError && firstLine(error) !== stepError) return null;
4302
+ }
4303
+ for (let i = index; i !== -1; i = parents[i] ?? -1) {
4304
+ const name = stepHookName(steps[i]);
4305
+ if (name) return { phase, hook: name };
4306
+ }
4307
+ return { phase, hook: phase };
4308
+ }
4309
+
4173
4310
  // ../core/src/describe-failure.ts
4174
4311
  var HEADLINE_MAX_CHARS = 120;
4175
4312
  var VALUE_MAX_CHARS = 40;
@@ -4516,7 +4653,27 @@ function buildCrash(parsed, opts) {
4516
4653
  }
4517
4654
  return line.text(what);
4518
4655
  }
4656
+ var CUSTOM_MESSAGE_MAX_CHARS = 60;
4657
+ var SENTENCE_CASE_WORD_RE = /^[A-Z][a-z]*(?![A-Za-z])/;
4658
+ function withLeads(line, parsed, ctx) {
4659
+ const hook = ctx?.failedIn && !parsed.timeoutPhase ? ctx.failedIn.hook : null;
4660
+ const message = parsed.customMessage ? truncateValue(parsed.customMessage, CUSTOM_MESSAGE_MAX_CHARS) : null;
4661
+ if (!hook && !message) return line;
4662
+ const led = new Line();
4663
+ if (hook) led.text(`In ${hook}${message ? ", " : ": "}`);
4664
+ if (message) led.text(`"${message}" failed: `);
4665
+ line.parts.forEach((part, i) => {
4666
+ const text = i === 0 && part.kind === "text" && SENTENCE_CASE_WORD_RE.test(part.text) ? part.text[0].toLowerCase() + part.text.slice(1) : part.text;
4667
+ if (part.kind === "text") led.text(text);
4668
+ else if (part.kind === "locator") led.locator(text);
4669
+ else led.value(text);
4670
+ });
4671
+ return led;
4672
+ }
4519
4673
  function build(parsed, opts, ctx) {
4674
+ return withLeads(buildLine(parsed, opts, ctx), parsed, ctx);
4675
+ }
4676
+ function buildLine(parsed, opts, ctx) {
4520
4677
  switch (parsed.kind) {
4521
4678
  case "action-timeout":
4522
4679
  return buildActionTimeout(parsed, opts);
@@ -4602,15 +4759,36 @@ function describeFailureText(raw, ctx) {
4602
4759
  if (!raw || !raw.trim()) return null;
4603
4760
  return describeFailure(parsePlaywrightError(raw, { stepParams: ctx?.stepParams }), ctx);
4604
4761
  }
4605
- function lastStepTitle(steps) {
4762
+ function headlineStep(steps) {
4606
4763
  if (!steps || steps.length === 0) return null;
4607
- const failed = steps.find((s) => s.failed);
4608
- return (failed ?? steps[steps.length - 1])?.title ?? null;
4764
+ const parents = stepParents(steps);
4765
+ const failed = failingStepIndex(steps, parents);
4766
+ if (failed !== null) return steps[failed];
4767
+ const phases = stepPhases(steps, parents);
4768
+ for (let i = steps.length - 1; i >= 0; i--) {
4769
+ const step = steps[i];
4770
+ if (phases[i] === "body" && !isCaptureStep(step) && !isPhaseContainer(step)) return step;
4771
+ }
4772
+ return steps[steps.length - 1];
4773
+ }
4774
+ function lastStepTitle(steps) {
4775
+ const title = headlineStep(steps)?.title;
4776
+ return typeof title === "string" ? title : null;
4777
+ }
4778
+ function failingStepParams(steps) {
4779
+ return headlineStep(steps)?.params ?? null;
4780
+ }
4781
+ function stepHeadlineContext(steps, error) {
4782
+ return {
4783
+ lastStepTitle: lastStepTitle(steps),
4784
+ stepParams: failingStepParams(steps),
4785
+ failedIn: steps && steps.length > 0 ? failureHookContext(steps, error ?? null) : null
4786
+ };
4609
4787
  }
4610
4788
 
4611
4789
  // src/internal/support/failure-links.ts
4612
4790
  function failureHeadline(error, steps) {
4613
- return describeFailureText(error, { lastStepTitle: lastStepTitle(steps) })?.headline ?? null;
4791
+ return describeFailureText(error, stepHeadlineContext(steps, error))?.headline ?? null;
4614
4792
  }
4615
4793
  function caseLocateUrl(serverUrl, runId, test) {
4616
4794
  const params = [
@@ -4946,6 +5124,13 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
4946
5124
  } catch {
4947
5125
  }
4948
5126
  }
5127
+ const pagesAttachment = this.options.captureLocators !== false ? result.attachments.find((a) => a.name === ATTACHMENT_NAMES.locatorPages) : void 0;
5128
+ if (pagesAttachment?.body) {
5129
+ try {
5130
+ testCase.locatorPages = JSON.parse(pagesAttachment.body.toString());
5131
+ } catch {
5132
+ }
5133
+ }
4949
5134
  }
4950
5135
  switch (status) {
4951
5136
  case "passed":
@@ -5143,7 +5328,30 @@ var import_node_zlib2 = require("zlib");
5143
5328
 
5144
5329
  // ../picker-dom/src/probe.ts
5145
5330
  function probeElementAttrs(el, arg) {
5146
- const { keep, tagRoles, inputRoles, roleSources, includeStructural, includeLabelText } = arg;
5331
+ const { keep, tagRoles, inputRoles, roleSources, includeStructural } = arg;
5332
+ const FIELD_TAGS = ["input", "select", "textarea"];
5333
+ const textWithout = (root, skip) => {
5334
+ if (root === skip) return "";
5335
+ if (root.nodeType === 3) return root.nodeValue || "";
5336
+ let text = "";
5337
+ const kids = root.childNodes || [];
5338
+ for (let i = 0; i < kids.length; i++) text += textWithout(kids[i], skip);
5339
+ return text;
5340
+ };
5341
+ const labelTextOf = (n) => {
5342
+ try {
5343
+ const ids = (n.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
5344
+ let text = "";
5345
+ for (const id of ids) {
5346
+ const ref = n.ownerDocument.getElementById(id);
5347
+ if (ref) text += ` ${textWithout(ref, n)}`;
5348
+ }
5349
+ if (!text.trim() && n.labels && n.labels.length > 0) text = textWithout(n.labels[0], n);
5350
+ return text.replace(/\s+/g, " ").trim().slice(0, 120) || null;
5351
+ } catch {
5352
+ return null;
5353
+ }
5354
+ };
5147
5355
  const attrMap = {};
5148
5356
  for (const key of keep) {
5149
5357
  const v = el.getAttribute(key) ?? el[key];
@@ -5223,10 +5431,18 @@ function probeElementAttrs(el, arg) {
5223
5431
  const targetRole = roleOf(el);
5224
5432
  const targetLevel = targetRole === "heading" ? levelOf(el) : null;
5225
5433
  const nameOf = (n) => {
5434
+ const labelledBy = !!n.getAttribute("aria-labelledby");
5435
+ const isField = FIELD_TAGS.indexOf((n.tagName || "").toLowerCase()) !== -1;
5436
+ const lt = labelledBy || isField ? labelTextOf(n) : null;
5437
+ if (lt && labelledBy) return lt;
5226
5438
  const al = n.getAttribute("aria-label");
5227
5439
  if (al) return al;
5228
- const txt = (n.textContent || "").replace(/\s+/g, " ").trim();
5229
- if (txt) return txt;
5440
+ if (isField) {
5441
+ if (lt) return lt;
5442
+ } else {
5443
+ const txt = (n.textContent || "").replace(/\s+/g, " ").trim();
5444
+ if (txt) return txt;
5445
+ }
5230
5446
  return n.getAttribute("title") || n.getAttribute("placeholder") || null;
5231
5447
  };
5232
5448
  const targetName = nameOf(el);
@@ -5418,16 +5634,7 @@ function probeElementAttrs(el, arg) {
5418
5634
  }
5419
5635
  }
5420
5636
  const hasLabel = !!(el.labels && el.labels.length > 0);
5421
- let labelText;
5422
- if (includeLabelText) {
5423
- labelText = null;
5424
- try {
5425
- const label = hasLabel ? el.labels[0] : null;
5426
- labelText = (label && label.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null;
5427
- } catch {
5428
- labelText = null;
5429
- }
5430
- }
5637
+ const labelText = labelTextOf(el);
5431
5638
  return {
5432
5639
  tagName: el.tagName?.toLowerCase?.() ?? "unknown",
5433
5640
  attributes: attrMap,
@@ -5439,7 +5646,7 @@ function probeElementAttrs(el, arg) {
5439
5646
  y: Math.round(r.y + r.height / 2)
5440
5647
  },
5441
5648
  hasLabel,
5442
- ...includeLabelText ? { labelText } : {},
5649
+ labelText,
5443
5650
  selectorCounts,
5444
5651
  ...includeStructural ? { rolePosition, ancestors } : {}
5445
5652
  };
@@ -6410,9 +6617,15 @@ function generateAlternatives(attrs) {
6410
6617
  }
6411
6618
  function approximateAccessibleName(attrs) {
6412
6619
  const a = attrs.attributes;
6620
+ const labelText = attrs.labelText || null;
6621
+ if (a["aria-labelledby"] && labelText) return labelText;
6413
6622
  const ariaLabel = a["aria-label"];
6414
6623
  if (ariaLabel) return ariaLabel;
6415
- if (attrs.textContent) return attrs.textContent;
6624
+ if (["input", "select", "textarea"].includes(attrs.tagName)) {
6625
+ if (labelText) return labelText;
6626
+ } else if (attrs.textContent) {
6627
+ return attrs.textContent;
6628
+ }
6416
6629
  const title = a["title"];
6417
6630
  if (title) return title;
6418
6631
  const placeholder = a["placeholder"];
@@ -6992,6 +7205,94 @@ function inventoryPageKey(url) {
6992
7205
  }
6993
7206
  }
6994
7207
 
7208
+ // ../core/src/page-key.ts
7209
+ function normalizeRoute(url) {
7210
+ try {
7211
+ const parsed = new URL(url);
7212
+ let pathname = parsed.pathname;
7213
+ pathname = pathname.replace(/\/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?=\/|$)/g, "/:jwt");
7214
+ 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");
7215
+ pathname = pathname.replace(/\/[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}(?=\/|$)/gi, "/:ulid");
7216
+ pathname = pathname.replace(/\/[A-Za-z0-9_-]{40,}(?=\/|$)/g, "/:token");
7217
+ pathname = pathname.replace(/\/[0-9a-f]{32,}(?=\/|$)/gi, "/:token");
7218
+ pathname = pathname.replace(/\/\d+(?=\/|$)/g, "/:id");
7219
+ let queryString = "";
7220
+ if (parsed.search) {
7221
+ const params = new URLSearchParams(parsed.search);
7222
+ const redacted = new URLSearchParams();
7223
+ for (const [key] of params) {
7224
+ redacted.set(key, "<redacted>");
7225
+ }
7226
+ queryString = "?" + redacted.toString();
7227
+ }
7228
+ return `${pathname}${queryString}`;
7229
+ } catch {
7230
+ return url;
7231
+ }
7232
+ }
7233
+ var PATH_ANCHOR_HOST = "piwi.invalid";
7234
+ function pageKey(url) {
7235
+ const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(url);
7236
+ if (hasScheme && !/^https?:\/\//i.test(url)) return null;
7237
+ const absolute = /^https?:\/\//i.test(url) ? url : `http://${PATH_ANCHOR_HOST}${url.startsWith("/") ? "" : "/"}${url}`;
7238
+ const normalized = normalizeRoute(absolute);
7239
+ const q = normalized.indexOf("?");
7240
+ return q < 0 ? normalized : normalized.slice(0, q);
7241
+ }
7242
+
7243
+ // src/internal/capture/locator-pages.ts
7244
+ var MAX_LOCATOR_PAGE_ENTRIES = 1e3;
7245
+ var MAX_LOCATOR_CHARS = 1e3;
7246
+ var NON_INTERACTIONS = /* @__PURE__ */ new Set(["waitFor"]);
7247
+ var interactions = /* @__PURE__ */ new WeakMap();
7248
+ function noteNavigation(page) {
7249
+ interactions.set(page, 0);
7250
+ }
7251
+ function isOnArrival(page) {
7252
+ return (interactions.get(page) ?? 0) === 0;
7253
+ }
7254
+ function noteLocatorCall(page, method, assertion = false) {
7255
+ const arrival = isOnArrival(page);
7256
+ if (!assertion && !NON_INTERACTIONS.has(method)) interactions.set(page, (interactions.get(page) ?? 0) + 1);
7257
+ return arrival;
7258
+ }
7259
+ function pageOf(url) {
7260
+ const key = pageKey(url);
7261
+ if (key === null) return null;
7262
+ try {
7263
+ const origin = new URL(url).origin;
7264
+ return origin && origin !== "null" ? { origin, page: key } : null;
7265
+ } catch {
7266
+ return null;
7267
+ }
7268
+ }
7269
+ var LocatorPageLog = class {
7270
+ constructor() {
7271
+ this.entries = /* @__PURE__ */ new Map();
7272
+ }
7273
+ /** Record one call. Silently skipped without a call site, off an http(s) page, or past the limits. */
7274
+ record(call) {
7275
+ if (!call.location || !call.locator || call.locator.length > MAX_LOCATOR_CHARS) return;
7276
+ const where = pageOf(call.url);
7277
+ if (!where) return;
7278
+ const key = `${call.location}\0${call.locator}\0${where.origin}\0${where.page}`;
7279
+ const existing = this.entries.get(key);
7280
+ if (existing) {
7281
+ existing.arrival ||= call.arrival;
7282
+ return;
7283
+ }
7284
+ if (this.entries.size >= MAX_LOCATOR_PAGE_ENTRIES) return;
7285
+ this.entries.set(key, { location: call.location, locator: call.locator, ...where, arrival: call.arrival });
7286
+ }
7287
+ get size() {
7288
+ return this.entries.size;
7289
+ }
7290
+ /** The entries in the order the calls first happened. */
7291
+ list() {
7292
+ return [...this.entries.values()];
7293
+ }
7294
+ };
7295
+
6995
7296
  // src/internal/probe/interception.ts
6996
7297
  var import_node_zlib = require("zlib");
6997
7298
 
@@ -7380,6 +7681,59 @@ function applyPickToSnapshots(snapshots, pick) {
7380
7681
  return true;
7381
7682
  }
7382
7683
 
7684
+ // src/internal/capture/quiet-capture.ts
7685
+ var import_node_module = require("module");
7686
+ var path20 = __toESM(require("path"));
7687
+ var BOXED_PREFIX_EXPORTS = [
7688
+ [
7689
+ "playwright-core/lib/coreBundle",
7690
+ (mod) => mod?.utils?.setBoxedStackPrefixes
7691
+ ],
7692
+ ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
7693
+ ];
7694
+ var boxing;
7695
+ function resolveBoxing() {
7696
+ try {
7697
+ const root = findOwnPackageJson(__dirname)?.root;
7698
+ if (!root) return null;
7699
+ const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
7700
+ const playwrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
7701
+ const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
7702
+ for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
7703
+ let set;
7704
+ try {
7705
+ set = pick(requireFromPlaywright(moduleId));
7706
+ } catch {
7707
+ continue;
7708
+ }
7709
+ if (typeof set !== "function") continue;
7710
+ return {
7711
+ set,
7712
+ prefixes: [path20.dirname(playwrightPackageJson), path20.join(root, "dist") + path20.sep]
7713
+ };
7714
+ }
7715
+ } catch {
7716
+ }
7717
+ return null;
7718
+ }
7719
+ function boxCaptureFrames(extraPrefixes = []) {
7720
+ const first = boxing === void 0;
7721
+ if (first) boxing = resolveBoxing();
7722
+ if (!boxing) return;
7723
+ const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
7724
+ if (!first && added.length === 0) return;
7725
+ boxing.prefixes.push(...added);
7726
+ try {
7727
+ boxing.set([...boxing.prefixes]);
7728
+ } catch {
7729
+ }
7730
+ }
7731
+ function internalCall(owner, run) {
7732
+ const wrap = owner?._wrapApiCall;
7733
+ if (typeof wrap !== "function") return run();
7734
+ return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
7735
+ }
7736
+
7383
7737
  // src/internal/capture/capture-fixtures.ts
7384
7738
  function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
7385
7739
  const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
@@ -7430,7 +7784,8 @@ function createSink() {
7430
7784
  probeItem: null,
7431
7785
  probeInterception: null,
7432
7786
  pickOffered: false,
7433
- userPick: null
7787
+ userPick: null,
7788
+ locatorPages: new LocatorPageLog()
7434
7789
  };
7435
7790
  }
7436
7791
  var currentSink = null;
@@ -7649,7 +8004,7 @@ async function recordPageWindow(sink, page) {
7649
8004
  if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
7650
8005
  if (key && !inventoriedPageKeys.has(key)) {
7651
8006
  inventoriedPageKeys.add(key);
7652
- const content = await readPageInventory(page);
8007
+ const content = await internalCall(page, () => readPageInventory(page));
7653
8008
  if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
7654
8009
  sink.pageInventories.push({
7655
8010
  url: content?.url ?? url,
@@ -7666,6 +8021,9 @@ async function stashPageState(sink, closing) {
7666
8021
  if (!page || isPageClosed(page)) return;
7667
8022
  const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
7668
8023
  if (!belongsToClosing) return;
8024
+ await internalCall(page, () => readPageBeforeClose(sink, page));
8025
+ }
8026
+ async function readPageBeforeClose(sink, page) {
7669
8027
  const vitals = await readWebVitals(page);
7670
8028
  if (vitals) sink.stashedWebVitals = vitals;
7671
8029
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
@@ -7707,7 +8065,10 @@ async function maybeOpenPicker(sink, closing) {
7707
8065
  console.log("[piwi] locator picker: no failing locator could be identified in this failure \u2014 nothing to replace.");
7708
8066
  return;
7709
8067
  }
7710
- const pick = await runLocatorPicker(page, testInfo, failed, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG });
8068
+ const pick = await internalCall(
8069
+ page,
8070
+ () => runLocatorPicker(page, testInfo, failed, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG })
8071
+ );
7711
8072
  if (!pick) return;
7712
8073
  sink.userPick = pick;
7713
8074
  applyPickToSnapshots(sink.capturedLocators, pick);
@@ -7726,12 +8087,8 @@ var CAPTURED_ATTRS_ARG = {
7726
8087
  // special-cased logic in the probe, so add them explicitly).
7727
8088
  roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
7728
8089
  // The reporter always wants ancestor-anchored alternatives (the picker's
7729
- // anchors step and generateAnchoredAlternatives both need them). `labelText`
7730
- // is what names a form field, and the probe reads it from `el.labels` in the
7731
- // same pass — so asking for it here is free and settles the accessible name
7732
- // of a labeled field without a second round trip (`exactAccessibleName`).
7733
- includeStructural: true,
7734
- includeLabelText: true
8090
+ // anchors step and generateAnchoredAlternatives both need them).
8091
+ includeStructural: true
7735
8092
  };
7736
8093
  var PROBE_GLOBAL = "__piwiProbeElement";
7737
8094
  var PROBE_INIT_SCRIPT = `(() => {
@@ -7742,15 +8099,18 @@ var PROBE_INIT_SCRIPT = `(() => {
7742
8099
  var PROBE_UNSEEDED_PAGES = /* @__PURE__ */ new WeakSet();
7743
8100
  function probeElement(page, target) {
7744
8101
  const shipSource = () => target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
7745
- if (!page || PROBE_UNSEEDED_PAGES.has(page)) return shipSource();
7746
- return target.evaluate((el, name) => {
7747
- const seeded = globalThis[name];
7748
- return typeof seeded === "function" ? seeded(el) : null;
7749
- }, PROBE_GLOBAL).then((attrs) => {
7750
- if (attrs) return attrs;
7751
- PROBE_UNSEEDED_PAGES.add(page);
7752
- return shipSource();
7753
- });
8102
+ if (!page || PROBE_UNSEEDED_PAGES.has(page)) return internalCall(page, shipSource);
8103
+ return internalCall(
8104
+ page,
8105
+ () => target.evaluate((el, name) => {
8106
+ const seeded = globalThis[name];
8107
+ return typeof seeded === "function" ? seeded(el) : null;
8108
+ }, PROBE_GLOBAL).then((attrs) => {
8109
+ if (attrs) return attrs;
8110
+ PROBE_UNSEEDED_PAGES.add(page);
8111
+ return shipSource();
8112
+ })
8113
+ );
7754
8114
  }
7755
8115
  async function ariaSnapshotBestEffort(target, timeout) {
7756
8116
  if (typeof target.ariaSnapshot !== "function") return null;
@@ -7801,7 +8161,7 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
7801
8161
  const role = resolveAriaRole({ ...attrs, accessibleName: null });
7802
8162
  const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
7803
8163
  const exactName = exactAccessibleName(attrs, role);
7804
- const aria = exactName === null && (role || isFormField) ? await ariaSnapshotBestEffort(target, 500) : null;
8164
+ const aria = exactName === null && (role || isFormField) ? await internalCall(page, () => ariaSnapshotBestEffort(target, 500)) : null;
7805
8165
  const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
7806
8166
  sink.capturedLocators[seq] = {
7807
8167
  location: callerLocation,
@@ -7829,15 +8189,23 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
7829
8189
  })();
7830
8190
  sink.capturePromises.push(resolveAttrs);
7831
8191
  }
8192
+ function callMethod(target, prop, args) {
8193
+ return target[prop](...args);
8194
+ }
8195
+ function recordLocatorPage(sink, page, locator, location, arrival) {
8196
+ try {
8197
+ sink.locatorPages.record({ location, locator: String(locator), url: page.url(), arrival });
8198
+ } catch {
8199
+ }
8200
+ }
7832
8201
  function wrapLocator(page, locator, originMethod, originArgs) {
7833
8202
  return new Proxy(locator, {
7834
8203
  get(target, prop) {
7835
8204
  const original = Reflect.get(target, prop);
7836
8205
  if (typeof original !== "function") return original;
7837
- const fn = original;
7838
8206
  if (CHAIN_METHOD_SET.has(prop)) {
7839
8207
  return (...args) => {
7840
- const next = fn.apply(target, args);
8208
+ const next = callMethod(target, prop, args);
7841
8209
  if (LOCATOR_CREATING_CHAINS.has(prop)) {
7842
8210
  return wrapLocator(page, next, String(prop), args);
7843
8211
  }
@@ -7849,11 +8217,12 @@ function wrapLocator(page, locator, originMethod, originArgs) {
7849
8217
  const sink = currentSink;
7850
8218
  const expression = typeof callArgs[0] === "string" ? callArgs[0] : "";
7851
8219
  const isNot = Boolean(callArgs[1]?.isNot);
8220
+ const callerLocation = sink ? captureCallerLocation() : null;
8221
+ if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
7852
8222
  if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
7853
- return fn.apply(target, callArgs);
8223
+ return callMethod(target, prop, callArgs);
7854
8224
  }
7855
8225
  sink.lastActivePage = page;
7856
- const callerLocation = captureCallerLocation();
7857
8226
  const used = {
7858
8227
  method: originMethod,
7859
8228
  args: originArgs,
@@ -7865,7 +8234,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
7865
8234
  seq = sink.capturedLocators.length;
7866
8235
  sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
7867
8236
  }
7868
- const result = await fn.apply(target, callArgs);
8237
+ const result = await callMethod(target, prop, callArgs);
7869
8238
  const matches = result?.matches;
7870
8239
  if (matches === true && !alreadyProbed) {
7871
8240
  if (callerLocation) sink.probedLocations.add(callerLocation);
@@ -7879,9 +8248,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
7879
8248
  if (!ACTION_METHOD_SET.has(prop)) return original;
7880
8249
  return async (...callArgs) => {
7881
8250
  const sink = currentSink;
7882
- if (!sink) return fn.apply(target, callArgs);
8251
+ if (!sink) return callMethod(target, prop, callArgs);
7883
8252
  sink.lastActivePage = page;
7884
8253
  const callerLocation = captureCallerLocation();
8254
+ recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, String(prop)));
7885
8255
  const used = {
7886
8256
  method: originMethod,
7887
8257
  args: originArgs,
@@ -7900,7 +8270,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
7900
8270
  }
7901
8271
  let result;
7902
8272
  try {
7903
- result = await fn.apply(target, callArgs);
8273
+ result = await callMethod(target, prop, callArgs);
7904
8274
  } catch (error) {
7905
8275
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
7906
8276
  throw error;
@@ -7919,11 +8289,10 @@ function wrapFrameLocator(page, frameLocator) {
7919
8289
  get(target, prop) {
7920
8290
  const original = Reflect.get(target, prop);
7921
8291
  if (typeof original !== "function") return original;
7922
- const fn = original;
7923
8292
  if (!LOCATOR_METHOD_SET.has(prop)) return original;
7924
8293
  return (...args) => {
7925
8294
  if (currentSink) currentSink.lastActivePage = page;
7926
- return wrapLocator(page, fn.apply(target, args), String(prop), args);
8295
+ return wrapLocator(page, callMethod(target, prop, args), String(prop), args);
7927
8296
  };
7928
8297
  }
7929
8298
  });
@@ -7970,7 +8339,13 @@ function instrumentPage(page) {
7970
8339
  };
7971
8340
  }
7972
8341
  if (typeof page.on === "function") {
7973
- page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
8342
+ page.on("framenavigated", (frame) => {
8343
+ PROBE_UNSEEDED_PAGES.delete(page);
8344
+ try {
8345
+ if (frame === page.mainFrame()) noteNavigation(page);
8346
+ } catch {
8347
+ }
8348
+ });
7974
8349
  }
7975
8350
  }
7976
8351
  if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
@@ -8109,6 +8484,12 @@ function patchBrowser(browser) {
8109
8484
  };
8110
8485
  }
8111
8486
  }
8487
+ function readRootAria(page) {
8488
+ return internalCall(page, () => ariaSnapshotBestEffort(page.locator(":root")));
8489
+ }
8490
+ function readRootAriaJson(page) {
8491
+ return internalCall(page, () => ariaSnapshotJSONBestEffort(page.locator(":root")));
8492
+ }
8112
8493
  async function flushSink(sink, testInfo) {
8113
8494
  await Promise.allSettled(sink.pendingHandlers);
8114
8495
  let drainDeadline;
@@ -8131,15 +8512,21 @@ async function flushSink(sink, testInfo) {
8131
8512
  body: Buffer.from(JSON.stringify(dedupeSnapshotsByLocation(sink.capturedLocators)))
8132
8513
  });
8133
8514
  }
8515
+ if (sink.locatorPages.size > 0) {
8516
+ await testInfo.attach(ATTACHMENT_NAMES.locatorPages, {
8517
+ contentType: "application/json",
8518
+ body: Buffer.from(JSON.stringify(sink.locatorPages.list()))
8519
+ });
8520
+ }
8134
8521
  if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
8135
8522
  try {
8136
- const snapshot = (pageReadable ? await ariaSnapshotBestEffort(page.locator(":root")) : null) ?? sink.stashedAria;
8523
+ const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
8137
8524
  if (snapshot) {
8138
8525
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
8139
8526
  contentType: "text/plain",
8140
8527
  body: snapshot
8141
8528
  });
8142
- const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
8529
+ const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
8143
8530
  if (snapshotJson) {
8144
8531
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
8145
8532
  contentType: "application/json",
@@ -8164,13 +8551,13 @@ async function flushSink(sink, testInfo) {
8164
8551
  }
8165
8552
  if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
8166
8553
  try {
8167
- const snapshot = (pageReadable ? await ariaSnapshotBestEffort(page.locator(":root")) : null) ?? sink.stashedAria;
8554
+ const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
8168
8555
  if (snapshot) {
8169
8556
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
8170
8557
  contentType: "text/plain",
8171
8558
  body: snapshot
8172
8559
  });
8173
- const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
8560
+ const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
8174
8561
  if (snapshotJson) {
8175
8562
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
8176
8563
  contentType: "application/json",
@@ -8216,7 +8603,7 @@ async function flushSink(sink, testInfo) {
8216
8603
  body: Buffer.from(JSON.stringify(sink.networkRequests))
8217
8604
  });
8218
8605
  }
8219
- const webVitals = (pageReadable ? await readWebVitals(page) : null) ?? sink.stashedWebVitals;
8606
+ const webVitals = (pageReadable ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
8220
8607
  if (webVitals) {
8221
8608
  await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
8222
8609
  contentType: "application/json",
@@ -8224,7 +8611,7 @@ async function flushSink(sink, testInfo) {
8224
8611
  });
8225
8612
  }
8226
8613
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
8227
- const pageState = (pageReadable ? await readPageState(page) : null) ?? sink.stashedPageState;
8614
+ const pageState = (pageReadable ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
8228
8615
  if (pageState) {
8229
8616
  await testInfo.attach(ATTACHMENT_NAMES.pageState, {
8230
8617
  contentType: "application/json",
@@ -8265,6 +8652,7 @@ var piwiFixtures = {
8265
8652
  // requested, so suites that never destructure `page` are still captured.
8266
8653
  piwiCapture: [
8267
8654
  async ({}, use, testInfo) => {
8655
+ boxCaptureFrames();
8268
8656
  const sink = createSink();
8269
8657
  sink.testInfo = testInfo;
8270
8658
  if (isProbeMode())
@@ -8309,12 +8697,12 @@ function extendPiwiFixtures(test) {
8309
8697
 
8310
8698
  // src/internal/ai/ai-fixtures.ts
8311
8699
  var fs18 = __toESM(require("fs"));
8312
- var path22 = __toESM(require("path"));
8700
+ var path23 = __toESM(require("path"));
8313
8701
  var import_test = require("@playwright/test");
8314
8702
 
8315
8703
  // src/internal/ai/artifact.ts
8316
8704
  var fs17 = __toESM(require("fs"));
8317
- var path20 = __toESM(require("path"));
8705
+ var path21 = __toESM(require("path"));
8318
8706
  var ARTIFACT_VERSION = 1;
8319
8707
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
8320
8708
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -8418,7 +8806,7 @@ function writeEntry(file, entry) {
8418
8806
  const canonical = serializeEntry(entry);
8419
8807
  return withEntryLock(file, () => {
8420
8808
  if (fs17.existsSync(file) && fs17.readFileSync(file, "utf8") === canonical) return { written: false };
8421
- fs17.mkdirSync(path20.dirname(file), { recursive: true });
8809
+ fs17.mkdirSync(path21.dirname(file), { recursive: true });
8422
8810
  const tmp = `${file}.${process.pid}.tmp`;
8423
8811
  fs17.writeFileSync(tmp, canonical);
8424
8812
  fs17.renameSync(tmp, file);
@@ -8427,7 +8815,7 @@ function writeEntry(file, entry) {
8427
8815
  }
8428
8816
  function withEntryLock(file, fn) {
8429
8817
  const lock = `${file}.lock`;
8430
- fs17.mkdirSync(path20.dirname(file), { recursive: true });
8818
+ fs17.mkdirSync(path21.dirname(file), { recursive: true });
8431
8819
  const deadline = Date.now() + 5e3;
8432
8820
  for (; ; ) {
8433
8821
  try {
@@ -8456,7 +8844,7 @@ function withEntryLock(file, fn) {
8456
8844
 
8457
8845
  // src/internal/ai/keys.ts
8458
8846
  var crypto3 = __toESM(require("crypto"));
8459
- var path21 = __toESM(require("path"));
8847
+ var path22 = __toESM(require("path"));
8460
8848
  var DEFAULT_AI_DIR = "__piwi__";
8461
8849
  function normalizeTemplate(template) {
8462
8850
  return template.trim().replace(/\s+/g, " ").toLowerCase();
@@ -8470,13 +8858,13 @@ function slug(text, maxLength = 40) {
8470
8858
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
8471
8859
  }
8472
8860
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
8473
- return path21.join(path21.dirname(specFile), dir, path21.basename(specFile));
8861
+ return path22.join(path22.dirname(specFile), dir, path22.basename(specFile));
8474
8862
  }
8475
8863
  function entryPath(params) {
8476
8864
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
8477
8865
  const hash = hashTemplate(basis, params.ordinal ?? 0);
8478
8866
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
8479
- return path21.join(entryDir(params.specFile, params.dir), name);
8867
+ return path22.join(entryDir(params.specFile, params.dir), name);
8480
8868
  }
8481
8869
  function parseLocation(location) {
8482
8870
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -8930,7 +9318,7 @@ function testIdentity(testInfo) {
8930
9318
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
8931
9319
  }
8932
9320
  function relativeToCwd(file) {
8933
- return path22.relative(process.cwd(), file).split(path22.sep).join("/");
9321
+ return path23.relative(process.cwd(), file).split(path23.sep).join("/");
8934
9322
  }
8935
9323
  function recordIntents(intents, entry) {
8936
9324
  const add = (locator, kind) => {