@piwitests/reporter 0.38.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",
@@ -1382,7 +1384,7 @@ var MetadataCollector = class {
1382
1384
  /**
1383
1385
  * Walk the test's parent `describe` suites to extract the suite path (titles)
1384
1386
  * and per-level config (parallel mode + annotations). Reaches into Playwright
1385
- * suite internals (`_parallelMode`, `_annotations`) — kept here next to
1387
+ * suite internals (`_parallelMode`, `_staticAnnotations`) — kept here next to
1386
1388
  * `getBrowserConfig` so all such access is guarded behind one class.
1387
1389
  */
1388
1390
  getSuiteInfo(test) {
@@ -1399,7 +1401,10 @@ var MetadataCollector = class {
1399
1401
  suitePath.push(s.title);
1400
1402
  const rawMode = s._parallelMode;
1401
1403
  const mode = rawMode === "parallel" ? "parallel" : rawMode === "serial" ? "serial" : "default";
1402
- const annotations = s._annotations ?? [];
1404
+ const staticAnnotations = s._staticAnnotations ?? [];
1405
+ const annotations = staticAnnotations.map(
1406
+ (a) => a.description === void 0 ? { type: a.type } : { type: a.type, description: a.description }
1407
+ );
1403
1408
  suiteConfig.push({ mode, annotations });
1404
1409
  }
1405
1410
  return { suitePath, suiteConfig };
@@ -1497,6 +1502,8 @@ var MetadataCollector = class {
1497
1502
  // The origin of `baseURL` is what the graph treats as the run's own
1498
1503
  // surface, so only requests to it become route nodes.
1499
1504
  baseURL: p.use?.baseURL,
1505
+ // The attribute `getByTestId` reads, so the locator index can resolve those locators on a live page.
1506
+ testIdAttribute: p.use?.testIdAttribute,
1500
1507
  viewport: p.use?.viewport,
1501
1508
  deviceScaleFactor: p.use?.deviceScaleFactor
1502
1509
  }
@@ -2141,11 +2148,14 @@ var MAX_STEP_PARAM_VALUE_CHARS = 200;
2141
2148
  function capStepValue(value, max) {
2142
2149
  return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}\u2026` : value;
2143
2150
  }
2151
+ var HTTP_METHOD_TITLE_RE = /^(?:get|post|put|patch|delete|head|options|fetch)$/i;
2144
2152
  function categorizeStep(title, pwCategory, params) {
2145
2153
  if (!title) return "other";
2146
- if (pwCategory === "hook" || pwCategory === "fixture") return pwCategory;
2154
+ if (pwCategory === "hook" || pwCategory === "fixture" || pwCategory === "test.step") return pwCategory;
2147
2155
  if (pwCategory === "expect") return "assertion";
2156
+ if (pwCategory === "test.attach") return "attach";
2148
2157
  const lower = title.toLowerCase();
2158
+ if (HTTP_METHOD_TITLE_RE.test(title.trim())) return "api";
2149
2159
  if (lower.startsWith("wait for") || lower.startsWith("locator.waitfor") || lower.startsWith("page.waitfor") || lower.startsWith("frame.waitfor"))
2150
2160
  return "wait";
2151
2161
  if (typeof params?.url === "string" && pwCategory !== "expect") return "navigation";
@@ -2183,13 +2193,14 @@ function normalizeStepParams(raw) {
2183
2193
  }
2184
2194
  return count > 0 ? out : void 0;
2185
2195
  }
2186
- function flattenSteps(steps) {
2196
+ function flattenSteps(steps, depth = 0) {
2187
2197
  const result = [];
2188
2198
  for (const step of steps) {
2189
2199
  const flat = {
2190
2200
  title: step.title,
2191
2201
  duration: step.duration,
2192
- category: categorizeStep(step.title, step.category, step.params)
2202
+ category: categorizeStep(step.title, step.category, step.params),
2203
+ depth
2193
2204
  };
2194
2205
  if (typeof step.subtitle === "string" && step.subtitle.length > 0) flat.subtitle = maskTokenLike(step.subtitle);
2195
2206
  const params = normalizeStepParams(step.params);
@@ -2201,7 +2212,7 @@ function flattenSteps(steps) {
2201
2212
  if (step.location) flat.location = `${step.location.file}:${step.location.line}:${step.location.column}`;
2202
2213
  if (step.startTime) flat.startTime = step.startTime instanceof Date ? step.startTime.getTime() : step.startTime;
2203
2214
  result.push(flat);
2204
- if (step.steps?.length > 0) result.push(...flattenSteps(step.steps));
2215
+ if (step.steps?.length > 0) result.push(...flattenSteps(step.steps, depth + 1));
2205
2216
  }
2206
2217
  return result;
2207
2218
  }
@@ -2313,7 +2324,8 @@ function findOwnPackageJson(fromDir) {
2313
2324
  const pkgPath = path8.resolve(fromDir, offset, "package.json");
2314
2325
  const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
2315
2326
  const { name, version } = pkg;
2316
- if (name === "@piwitests/reporter" && typeof version === "string") return { version };
2327
+ if (name === "@piwitests/reporter" && typeof version === "string")
2328
+ return { version, root: path8.dirname(pkgPath) };
2317
2329
  } catch {
2318
2330
  }
2319
2331
  }
@@ -2767,8 +2779,8 @@ function parseRouteKey(routeKey) {
2767
2779
  return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
2768
2780
  }
2769
2781
  function patternToRegex(pattern) {
2770
- const path23 = pattern.split("?")[0] ?? pattern;
2771
- 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, "[^/]+");
2772
2784
  return new RegExp(`^${escaped}$`);
2773
2785
  }
2774
2786
  function suitePathEqual(a, b) {
@@ -2791,13 +2803,13 @@ function matchProbeItem(plan, test) {
2791
2803
  function requestMatchesRoute(method, url, routeKey) {
2792
2804
  const { method: rm, pattern } = parseRouteKey(routeKey);
2793
2805
  if (rm && rm !== method.toUpperCase()) return false;
2794
- let path23;
2806
+ let path24;
2795
2807
  try {
2796
- path23 = new URL(url).pathname;
2808
+ path24 = new URL(url).pathname;
2797
2809
  } catch {
2798
- path23 = url.split("?")[0] ?? url;
2810
+ path24 = url.split("?")[0] ?? url;
2799
2811
  }
2800
- return patternToRegex(pattern).test(path23);
2812
+ return patternToRegex(pattern).test(path24);
2801
2813
  }
2802
2814
  function createProbeState() {
2803
2815
  return { navigated: false, matches: 0 };
@@ -2819,10 +2831,10 @@ function isProbeMode() {
2819
2831
  function loadPlan() {
2820
2832
  if (planCache !== void 0) return planCache;
2821
2833
  planCache = null;
2822
- const path23 = process.env[PIWI_PROBE_ENV.plan];
2823
- if (path23) {
2834
+ const path24 = process.env[PIWI_PROBE_ENV.plan];
2835
+ if (path24) {
2824
2836
  try {
2825
- planCache = JSON.parse(fs14.readFileSync(path23, "utf8"));
2837
+ planCache = JSON.parse(fs14.readFileSync(path24, "utf8"));
2826
2838
  } catch {
2827
2839
  planCache = null;
2828
2840
  }
@@ -2844,10 +2856,10 @@ function outcomeFromStatus(status, applied) {
2844
2856
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2845
2857
  }
2846
2858
  function recordProbeOutcome(line) {
2847
- const path23 = process.env[PIWI_PROBE_ENV.results];
2848
- if (!path23) return;
2859
+ const path24 = process.env[PIWI_PROBE_ENV.results];
2860
+ if (!path24) return;
2849
2861
  try {
2850
- fs14.appendFileSync(path23, `${JSON.stringify(line)}
2862
+ fs14.appendFileSync(path24, `${JSON.stringify(line)}
2851
2863
  `);
2852
2864
  } catch {
2853
2865
  }
@@ -4084,12 +4096,20 @@ function readUrl(text, callLog, received) {
4084
4096
  }
4085
4097
  return null;
4086
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
+ }
4087
4107
  function parsePlaywrightError(raw, context) {
4088
4108
  const clean = stripAnsi(raw ?? "").replace(/\r\n?/g, "\n");
4089
4109
  const text = withoutStackFrames(clean);
4090
4110
  const messageHead = extractMessageHead(clean);
4091
- const firstLine = messageHead.split("\n")[0] ?? "";
4092
- 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;
4093
4113
  const strict = STRICT_RE.exec(text);
4094
4114
  const call = CALL_RE.exec(text);
4095
4115
  const callLog = readCallLog(text);
@@ -4159,12 +4179,134 @@ function parsePlaywrightError(raw, context) {
4159
4179
  lastCallLogLine: callLog.lastLine,
4160
4180
  lastStateLine: callLog.stateLine,
4161
4181
  messageHead,
4182
+ customMessage: isAssertion ? readCustomMessage(messageHead) : null,
4162
4183
  topFrame: frame ? `${frame.file}:${frame.line}` : null,
4163
4184
  isNavigationFailure,
4164
4185
  isLocatorResolutionFailure
4165
4186
  };
4166
4187
  }
4167
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
+
4168
4310
  // ../core/src/describe-failure.ts
4169
4311
  var HEADLINE_MAX_CHARS = 120;
4170
4312
  var VALUE_MAX_CHARS = 40;
@@ -4511,7 +4653,27 @@ function buildCrash(parsed, opts) {
4511
4653
  }
4512
4654
  return line.text(what);
4513
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
+ }
4514
4673
  function build(parsed, opts, ctx) {
4674
+ return withLeads(buildLine(parsed, opts, ctx), parsed, ctx);
4675
+ }
4676
+ function buildLine(parsed, opts, ctx) {
4515
4677
  switch (parsed.kind) {
4516
4678
  case "action-timeout":
4517
4679
  return buildActionTimeout(parsed, opts);
@@ -4597,15 +4759,36 @@ function describeFailureText(raw, ctx) {
4597
4759
  if (!raw || !raw.trim()) return null;
4598
4760
  return describeFailure(parsePlaywrightError(raw, { stepParams: ctx?.stepParams }), ctx);
4599
4761
  }
4600
- function lastStepTitle(steps) {
4762
+ function headlineStep(steps) {
4601
4763
  if (!steps || steps.length === 0) return null;
4602
- const failed = steps.find((s) => s.failed);
4603
- 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
+ };
4604
4787
  }
4605
4788
 
4606
4789
  // src/internal/support/failure-links.ts
4607
4790
  function failureHeadline(error, steps) {
4608
- return describeFailureText(error, { lastStepTitle: lastStepTitle(steps) })?.headline ?? null;
4791
+ return describeFailureText(error, stepHeadlineContext(steps, error))?.headline ?? null;
4609
4792
  }
4610
4793
  function caseLocateUrl(serverUrl, runId, test) {
4611
4794
  const params = [
@@ -4941,6 +5124,13 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
4941
5124
  } catch {
4942
5125
  }
4943
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
+ }
4944
5134
  }
4945
5135
  switch (status) {
4946
5136
  case "passed":
@@ -5138,7 +5328,30 @@ var import_node_zlib2 = require("zlib");
5138
5328
 
5139
5329
  // ../picker-dom/src/probe.ts
5140
5330
  function probeElementAttrs(el, arg) {
5141
- 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
+ };
5142
5355
  const attrMap = {};
5143
5356
  for (const key of keep) {
5144
5357
  const v = el.getAttribute(key) ?? el[key];
@@ -5218,10 +5431,18 @@ function probeElementAttrs(el, arg) {
5218
5431
  const targetRole = roleOf(el);
5219
5432
  const targetLevel = targetRole === "heading" ? levelOf(el) : null;
5220
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;
5221
5438
  const al = n.getAttribute("aria-label");
5222
5439
  if (al) return al;
5223
- const txt = (n.textContent || "").replace(/\s+/g, " ").trim();
5224
- 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
+ }
5225
5446
  return n.getAttribute("title") || n.getAttribute("placeholder") || null;
5226
5447
  };
5227
5448
  const targetName = nameOf(el);
@@ -5413,16 +5634,7 @@ function probeElementAttrs(el, arg) {
5413
5634
  }
5414
5635
  }
5415
5636
  const hasLabel = !!(el.labels && el.labels.length > 0);
5416
- let labelText;
5417
- if (includeLabelText) {
5418
- labelText = null;
5419
- try {
5420
- const label = hasLabel ? el.labels[0] : null;
5421
- labelText = (label && label.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null;
5422
- } catch {
5423
- labelText = null;
5424
- }
5425
- }
5637
+ const labelText = labelTextOf(el);
5426
5638
  return {
5427
5639
  tagName: el.tagName?.toLowerCase?.() ?? "unknown",
5428
5640
  attributes: attrMap,
@@ -5434,7 +5646,7 @@ function probeElementAttrs(el, arg) {
5434
5646
  y: Math.round(r.y + r.height / 2)
5435
5647
  },
5436
5648
  hasLabel,
5437
- ...includeLabelText ? { labelText } : {},
5649
+ labelText,
5438
5650
  selectorCounts,
5439
5651
  ...includeStructural ? { rolePosition, ancestors } : {}
5440
5652
  };
@@ -6405,9 +6617,15 @@ function generateAlternatives(attrs) {
6405
6617
  }
6406
6618
  function approximateAccessibleName(attrs) {
6407
6619
  const a = attrs.attributes;
6620
+ const labelText = attrs.labelText || null;
6621
+ if (a["aria-labelledby"] && labelText) return labelText;
6408
6622
  const ariaLabel = a["aria-label"];
6409
6623
  if (ariaLabel) return ariaLabel;
6410
- 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
+ }
6411
6629
  const title = a["title"];
6412
6630
  if (title) return title;
6413
6631
  const placeholder = a["placeholder"];
@@ -6987,6 +7205,94 @@ function inventoryPageKey(url) {
6987
7205
  }
6988
7206
  }
6989
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
+
6990
7296
  // src/internal/probe/interception.ts
6991
7297
  var import_node_zlib = require("zlib");
6992
7298
 
@@ -7375,6 +7681,59 @@ function applyPickToSnapshots(snapshots, pick) {
7375
7681
  return true;
7376
7682
  }
7377
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
+
7378
7737
  // src/internal/capture/capture-fixtures.ts
7379
7738
  function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
7380
7739
  const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
@@ -7425,7 +7784,8 @@ function createSink() {
7425
7784
  probeItem: null,
7426
7785
  probeInterception: null,
7427
7786
  pickOffered: false,
7428
- userPick: null
7787
+ userPick: null,
7788
+ locatorPages: new LocatorPageLog()
7429
7789
  };
7430
7790
  }
7431
7791
  var currentSink = null;
@@ -7644,7 +8004,7 @@ async function recordPageWindow(sink, page) {
7644
8004
  if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
7645
8005
  if (key && !inventoriedPageKeys.has(key)) {
7646
8006
  inventoriedPageKeys.add(key);
7647
- const content = await readPageInventory(page);
8007
+ const content = await internalCall(page, () => readPageInventory(page));
7648
8008
  if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
7649
8009
  sink.pageInventories.push({
7650
8010
  url: content?.url ?? url,
@@ -7661,6 +8021,9 @@ async function stashPageState(sink, closing) {
7661
8021
  if (!page || isPageClosed(page)) return;
7662
8022
  const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
7663
8023
  if (!belongsToClosing) return;
8024
+ await internalCall(page, () => readPageBeforeClose(sink, page));
8025
+ }
8026
+ async function readPageBeforeClose(sink, page) {
7664
8027
  const vitals = await readWebVitals(page);
7665
8028
  if (vitals) sink.stashedWebVitals = vitals;
7666
8029
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
@@ -7702,7 +8065,10 @@ async function maybeOpenPicker(sink, closing) {
7702
8065
  console.log("[piwi] locator picker: no failing locator could be identified in this failure \u2014 nothing to replace.");
7703
8066
  return;
7704
8067
  }
7705
- 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
+ );
7706
8072
  if (!pick) return;
7707
8073
  sink.userPick = pick;
7708
8074
  applyPickToSnapshots(sink.capturedLocators, pick);
@@ -7721,12 +8087,8 @@ var CAPTURED_ATTRS_ARG = {
7721
8087
  // special-cased logic in the probe, so add them explicitly).
7722
8088
  roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
7723
8089
  // The reporter always wants ancestor-anchored alternatives (the picker's
7724
- // anchors step and generateAnchoredAlternatives both need them). `labelText`
7725
- // is what names a form field, and the probe reads it from `el.labels` in the
7726
- // same pass — so asking for it here is free and settles the accessible name
7727
- // of a labeled field without a second round trip (`exactAccessibleName`).
7728
- includeStructural: true,
7729
- includeLabelText: true
8090
+ // anchors step and generateAnchoredAlternatives both need them).
8091
+ includeStructural: true
7730
8092
  };
7731
8093
  var PROBE_GLOBAL = "__piwiProbeElement";
7732
8094
  var PROBE_INIT_SCRIPT = `(() => {
@@ -7737,15 +8099,18 @@ var PROBE_INIT_SCRIPT = `(() => {
7737
8099
  var PROBE_UNSEEDED_PAGES = /* @__PURE__ */ new WeakSet();
7738
8100
  function probeElement(page, target) {
7739
8101
  const shipSource = () => target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
7740
- if (!page || PROBE_UNSEEDED_PAGES.has(page)) return shipSource();
7741
- return target.evaluate((el, name) => {
7742
- const seeded = globalThis[name];
7743
- return typeof seeded === "function" ? seeded(el) : null;
7744
- }, PROBE_GLOBAL).then((attrs) => {
7745
- if (attrs) return attrs;
7746
- PROBE_UNSEEDED_PAGES.add(page);
7747
- return shipSource();
7748
- });
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
+ );
7749
8114
  }
7750
8115
  async function ariaSnapshotBestEffort(target, timeout) {
7751
8116
  if (typeof target.ariaSnapshot !== "function") return null;
@@ -7796,7 +8161,7 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
7796
8161
  const role = resolveAriaRole({ ...attrs, accessibleName: null });
7797
8162
  const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
7798
8163
  const exactName = exactAccessibleName(attrs, role);
7799
- 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;
7800
8165
  const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
7801
8166
  sink.capturedLocators[seq] = {
7802
8167
  location: callerLocation,
@@ -7824,15 +8189,23 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
7824
8189
  })();
7825
8190
  sink.capturePromises.push(resolveAttrs);
7826
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
+ }
7827
8201
  function wrapLocator(page, locator, originMethod, originArgs) {
7828
8202
  return new Proxy(locator, {
7829
8203
  get(target, prop) {
7830
8204
  const original = Reflect.get(target, prop);
7831
8205
  if (typeof original !== "function") return original;
7832
- const fn = original;
7833
8206
  if (CHAIN_METHOD_SET.has(prop)) {
7834
8207
  return (...args) => {
7835
- const next = fn.apply(target, args);
8208
+ const next = callMethod(target, prop, args);
7836
8209
  if (LOCATOR_CREATING_CHAINS.has(prop)) {
7837
8210
  return wrapLocator(page, next, String(prop), args);
7838
8211
  }
@@ -7844,11 +8217,12 @@ function wrapLocator(page, locator, originMethod, originArgs) {
7844
8217
  const sink = currentSink;
7845
8218
  const expression = typeof callArgs[0] === "string" ? callArgs[0] : "";
7846
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));
7847
8222
  if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
7848
- return fn.apply(target, callArgs);
8223
+ return callMethod(target, prop, callArgs);
7849
8224
  }
7850
8225
  sink.lastActivePage = page;
7851
- const callerLocation = captureCallerLocation();
7852
8226
  const used = {
7853
8227
  method: originMethod,
7854
8228
  args: originArgs,
@@ -7860,7 +8234,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
7860
8234
  seq = sink.capturedLocators.length;
7861
8235
  sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
7862
8236
  }
7863
- const result = await fn.apply(target, callArgs);
8237
+ const result = await callMethod(target, prop, callArgs);
7864
8238
  const matches = result?.matches;
7865
8239
  if (matches === true && !alreadyProbed) {
7866
8240
  if (callerLocation) sink.probedLocations.add(callerLocation);
@@ -7874,9 +8248,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
7874
8248
  if (!ACTION_METHOD_SET.has(prop)) return original;
7875
8249
  return async (...callArgs) => {
7876
8250
  const sink = currentSink;
7877
- if (!sink) return fn.apply(target, callArgs);
8251
+ if (!sink) return callMethod(target, prop, callArgs);
7878
8252
  sink.lastActivePage = page;
7879
8253
  const callerLocation = captureCallerLocation();
8254
+ recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, String(prop)));
7880
8255
  const used = {
7881
8256
  method: originMethod,
7882
8257
  args: originArgs,
@@ -7895,7 +8270,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
7895
8270
  }
7896
8271
  let result;
7897
8272
  try {
7898
- result = await fn.apply(target, callArgs);
8273
+ result = await callMethod(target, prop, callArgs);
7899
8274
  } catch (error) {
7900
8275
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
7901
8276
  throw error;
@@ -7914,11 +8289,10 @@ function wrapFrameLocator(page, frameLocator) {
7914
8289
  get(target, prop) {
7915
8290
  const original = Reflect.get(target, prop);
7916
8291
  if (typeof original !== "function") return original;
7917
- const fn = original;
7918
8292
  if (!LOCATOR_METHOD_SET.has(prop)) return original;
7919
8293
  return (...args) => {
7920
8294
  if (currentSink) currentSink.lastActivePage = page;
7921
- return wrapLocator(page, fn.apply(target, args), String(prop), args);
8295
+ return wrapLocator(page, callMethod(target, prop, args), String(prop), args);
7922
8296
  };
7923
8297
  }
7924
8298
  });
@@ -7965,7 +8339,13 @@ function instrumentPage(page) {
7965
8339
  };
7966
8340
  }
7967
8341
  if (typeof page.on === "function") {
7968
- 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
+ });
7969
8349
  }
7970
8350
  }
7971
8351
  if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
@@ -8104,6 +8484,12 @@ function patchBrowser(browser) {
8104
8484
  };
8105
8485
  }
8106
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
+ }
8107
8493
  async function flushSink(sink, testInfo) {
8108
8494
  await Promise.allSettled(sink.pendingHandlers);
8109
8495
  let drainDeadline;
@@ -8126,15 +8512,21 @@ async function flushSink(sink, testInfo) {
8126
8512
  body: Buffer.from(JSON.stringify(dedupeSnapshotsByLocation(sink.capturedLocators)))
8127
8513
  });
8128
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
+ }
8129
8521
  if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
8130
8522
  try {
8131
- const snapshot = (pageReadable ? await ariaSnapshotBestEffort(page.locator(":root")) : null) ?? sink.stashedAria;
8523
+ const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
8132
8524
  if (snapshot) {
8133
8525
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
8134
8526
  contentType: "text/plain",
8135
8527
  body: snapshot
8136
8528
  });
8137
- const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
8529
+ const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
8138
8530
  if (snapshotJson) {
8139
8531
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
8140
8532
  contentType: "application/json",
@@ -8159,13 +8551,13 @@ async function flushSink(sink, testInfo) {
8159
8551
  }
8160
8552
  if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
8161
8553
  try {
8162
- const snapshot = (pageReadable ? await ariaSnapshotBestEffort(page.locator(":root")) : null) ?? sink.stashedAria;
8554
+ const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
8163
8555
  if (snapshot) {
8164
8556
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
8165
8557
  contentType: "text/plain",
8166
8558
  body: snapshot
8167
8559
  });
8168
- const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
8560
+ const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
8169
8561
  if (snapshotJson) {
8170
8562
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
8171
8563
  contentType: "application/json",
@@ -8211,7 +8603,7 @@ async function flushSink(sink, testInfo) {
8211
8603
  body: Buffer.from(JSON.stringify(sink.networkRequests))
8212
8604
  });
8213
8605
  }
8214
- const webVitals = (pageReadable ? await readWebVitals(page) : null) ?? sink.stashedWebVitals;
8606
+ const webVitals = (pageReadable ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
8215
8607
  if (webVitals) {
8216
8608
  await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
8217
8609
  contentType: "application/json",
@@ -8219,7 +8611,7 @@ async function flushSink(sink, testInfo) {
8219
8611
  });
8220
8612
  }
8221
8613
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
8222
- const pageState = (pageReadable ? await readPageState(page) : null) ?? sink.stashedPageState;
8614
+ const pageState = (pageReadable ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
8223
8615
  if (pageState) {
8224
8616
  await testInfo.attach(ATTACHMENT_NAMES.pageState, {
8225
8617
  contentType: "application/json",
@@ -8260,6 +8652,7 @@ var piwiFixtures = {
8260
8652
  // requested, so suites that never destructure `page` are still captured.
8261
8653
  piwiCapture: [
8262
8654
  async ({}, use, testInfo) => {
8655
+ boxCaptureFrames();
8263
8656
  const sink = createSink();
8264
8657
  sink.testInfo = testInfo;
8265
8658
  if (isProbeMode())
@@ -8304,12 +8697,12 @@ function extendPiwiFixtures(test) {
8304
8697
 
8305
8698
  // src/internal/ai/ai-fixtures.ts
8306
8699
  var fs18 = __toESM(require("fs"));
8307
- var path22 = __toESM(require("path"));
8700
+ var path23 = __toESM(require("path"));
8308
8701
  var import_test = require("@playwright/test");
8309
8702
 
8310
8703
  // src/internal/ai/artifact.ts
8311
8704
  var fs17 = __toESM(require("fs"));
8312
- var path20 = __toESM(require("path"));
8705
+ var path21 = __toESM(require("path"));
8313
8706
  var ARTIFACT_VERSION = 1;
8314
8707
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
8315
8708
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -8413,7 +8806,7 @@ function writeEntry(file, entry) {
8413
8806
  const canonical = serializeEntry(entry);
8414
8807
  return withEntryLock(file, () => {
8415
8808
  if (fs17.existsSync(file) && fs17.readFileSync(file, "utf8") === canonical) return { written: false };
8416
- fs17.mkdirSync(path20.dirname(file), { recursive: true });
8809
+ fs17.mkdirSync(path21.dirname(file), { recursive: true });
8417
8810
  const tmp = `${file}.${process.pid}.tmp`;
8418
8811
  fs17.writeFileSync(tmp, canonical);
8419
8812
  fs17.renameSync(tmp, file);
@@ -8422,7 +8815,7 @@ function writeEntry(file, entry) {
8422
8815
  }
8423
8816
  function withEntryLock(file, fn) {
8424
8817
  const lock = `${file}.lock`;
8425
- fs17.mkdirSync(path20.dirname(file), { recursive: true });
8818
+ fs17.mkdirSync(path21.dirname(file), { recursive: true });
8426
8819
  const deadline = Date.now() + 5e3;
8427
8820
  for (; ; ) {
8428
8821
  try {
@@ -8451,7 +8844,7 @@ function withEntryLock(file, fn) {
8451
8844
 
8452
8845
  // src/internal/ai/keys.ts
8453
8846
  var crypto3 = __toESM(require("crypto"));
8454
- var path21 = __toESM(require("path"));
8847
+ var path22 = __toESM(require("path"));
8455
8848
  var DEFAULT_AI_DIR = "__piwi__";
8456
8849
  function normalizeTemplate(template) {
8457
8850
  return template.trim().replace(/\s+/g, " ").toLowerCase();
@@ -8465,13 +8858,13 @@ function slug(text, maxLength = 40) {
8465
8858
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
8466
8859
  }
8467
8860
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
8468
- return path21.join(path21.dirname(specFile), dir, path21.basename(specFile));
8861
+ return path22.join(path22.dirname(specFile), dir, path22.basename(specFile));
8469
8862
  }
8470
8863
  function entryPath(params) {
8471
8864
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
8472
8865
  const hash = hashTemplate(basis, params.ordinal ?? 0);
8473
8866
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
8474
- return path21.join(entryDir(params.specFile, params.dir), name);
8867
+ return path22.join(entryDir(params.specFile, params.dir), name);
8475
8868
  }
8476
8869
  function parseLocation(location) {
8477
8870
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -8925,7 +9318,7 @@ function testIdentity(testInfo) {
8925
9318
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
8926
9319
  }
8927
9320
  function relativeToCwd(file) {
8928
- return path22.relative(process.cwd(), file).split(path22.sep).join("/");
9321
+ return path23.relative(process.cwd(), file).split(path23.sep).join("/");
8929
9322
  }
8930
9323
  function recordIntents(intents, entry) {
8931
9324
  const add = (locator, kind) => {