@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.
@@ -31,11 +31,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var capture_fixtures_exports = {};
32
32
  __export(capture_fixtures_exports, {
33
33
  CAPTURED_ATTRS_ARG: () => CAPTURED_ATTRS_ARG,
34
+ LocatorPageLog: () => LocatorPageLog,
34
35
  ariaSnapshotBestEffort: () => ariaSnapshotBestEffort,
35
36
  ariaSnapshotJSONBestEffort: () => ariaSnapshotJSONBestEffort,
37
+ boxCaptureFrames: () => boxCaptureFrames,
36
38
  buildPageState: () => buildPageState,
39
+ callMethod: () => callMethod,
37
40
  computeCoreVitals: () => computeCoreVitals,
38
41
  extendPiwiFixtures: () => extendPiwiFixtures,
42
+ internalCall: () => internalCall,
43
+ noteLocatorCall: () => noteLocatorCall,
44
+ noteNavigation: () => noteNavigation,
39
45
  piwiFixtures: () => piwiFixtures,
40
46
  probeElementAttrs: () => probeElementAttrs
41
47
  });
@@ -44,7 +50,30 @@ var import_node_zlib2 = require("zlib");
44
50
 
45
51
  // ../picker-dom/src/probe.ts
46
52
  function probeElementAttrs(el, arg) {
47
- const { keep, tagRoles, inputRoles, roleSources, includeStructural, includeLabelText } = arg;
53
+ const { keep, tagRoles, inputRoles, roleSources, includeStructural } = arg;
54
+ const FIELD_TAGS = ["input", "select", "textarea"];
55
+ const textWithout = (root, skip) => {
56
+ if (root === skip) return "";
57
+ if (root.nodeType === 3) return root.nodeValue || "";
58
+ let text = "";
59
+ const kids = root.childNodes || [];
60
+ for (let i = 0; i < kids.length; i++) text += textWithout(kids[i], skip);
61
+ return text;
62
+ };
63
+ const labelTextOf = (n) => {
64
+ try {
65
+ const ids = (n.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
66
+ let text = "";
67
+ for (const id of ids) {
68
+ const ref = n.ownerDocument.getElementById(id);
69
+ if (ref) text += ` ${textWithout(ref, n)}`;
70
+ }
71
+ if (!text.trim() && n.labels && n.labels.length > 0) text = textWithout(n.labels[0], n);
72
+ return text.replace(/\s+/g, " ").trim().slice(0, 120) || null;
73
+ } catch {
74
+ return null;
75
+ }
76
+ };
48
77
  const attrMap = {};
49
78
  for (const key of keep) {
50
79
  const v = el.getAttribute(key) ?? el[key];
@@ -124,10 +153,18 @@ function probeElementAttrs(el, arg) {
124
153
  const targetRole = roleOf(el);
125
154
  const targetLevel = targetRole === "heading" ? levelOf(el) : null;
126
155
  const nameOf = (n) => {
156
+ const labelledBy = !!n.getAttribute("aria-labelledby");
157
+ const isField = FIELD_TAGS.indexOf((n.tagName || "").toLowerCase()) !== -1;
158
+ const lt = labelledBy || isField ? labelTextOf(n) : null;
159
+ if (lt && labelledBy) return lt;
127
160
  const al = n.getAttribute("aria-label");
128
161
  if (al) return al;
129
- const txt = (n.textContent || "").replace(/\s+/g, " ").trim();
130
- if (txt) return txt;
162
+ if (isField) {
163
+ if (lt) return lt;
164
+ } else {
165
+ const txt = (n.textContent || "").replace(/\s+/g, " ").trim();
166
+ if (txt) return txt;
167
+ }
131
168
  return n.getAttribute("title") || n.getAttribute("placeholder") || null;
132
169
  };
133
170
  const targetName = nameOf(el);
@@ -319,16 +356,7 @@ function probeElementAttrs(el, arg) {
319
356
  }
320
357
  }
321
358
  const hasLabel = !!(el.labels && el.labels.length > 0);
322
- let labelText;
323
- if (includeLabelText) {
324
- labelText = null;
325
- try {
326
- const label = hasLabel ? el.labels[0] : null;
327
- labelText = (label && label.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null;
328
- } catch {
329
- labelText = null;
330
- }
331
- }
359
+ const labelText = labelTextOf(el);
332
360
  return {
333
361
  tagName: el.tagName?.toLowerCase?.() ?? "unknown",
334
362
  attributes: attrMap,
@@ -340,7 +368,7 @@ function probeElementAttrs(el, arg) {
340
368
  y: Math.round(r.y + r.height / 2)
341
369
  },
342
370
  hasLabel,
343
- ...includeLabelText ? { labelText } : {},
371
+ labelText,
344
372
  selectorCounts,
345
373
  ...includeStructural ? { rolePosition, ancestors } : {}
346
374
  };
@@ -1311,9 +1339,15 @@ function generateAlternatives(attrs) {
1311
1339
  }
1312
1340
  function approximateAccessibleName(attrs) {
1313
1341
  const a = attrs.attributes;
1342
+ const labelText = attrs.labelText || null;
1343
+ if (a["aria-labelledby"] && labelText) return labelText;
1314
1344
  const ariaLabel = a["aria-label"];
1315
1345
  if (ariaLabel) return ariaLabel;
1316
- if (attrs.textContent) return attrs.textContent;
1346
+ if (["input", "select", "textarea"].includes(attrs.tagName)) {
1347
+ if (labelText) return labelText;
1348
+ } else if (attrs.textContent) {
1349
+ return attrs.textContent;
1350
+ }
1317
1351
  const title = a["title"];
1318
1352
  if (title) return title;
1319
1353
  const placeholder = a["placeholder"];
@@ -1743,6 +1777,7 @@ var ATTACHMENT_NAMES = {
1743
1777
  dialogs: "piwi-dialogs",
1744
1778
  network: "piwi-network",
1745
1779
  pageInventory: "piwi-page-inventory",
1780
+ locatorPages: "piwi-locator-pages",
1746
1781
  webVitals: "piwi-web-vitals",
1747
1782
  locatorSuggestion: "piwi-locator-suggestion",
1748
1783
  pageState: "piwi-page-state",
@@ -1869,6 +1904,94 @@ function inventoryPageKey(url) {
1869
1904
  }
1870
1905
  }
1871
1906
 
1907
+ // ../core/src/page-key.ts
1908
+ function normalizeRoute(url) {
1909
+ try {
1910
+ const parsed = new URL(url);
1911
+ let pathname = parsed.pathname;
1912
+ pathname = pathname.replace(/\/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?=\/|$)/g, "/:jwt");
1913
+ 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");
1914
+ pathname = pathname.replace(/\/[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}(?=\/|$)/gi, "/:ulid");
1915
+ pathname = pathname.replace(/\/[A-Za-z0-9_-]{40,}(?=\/|$)/g, "/:token");
1916
+ pathname = pathname.replace(/\/[0-9a-f]{32,}(?=\/|$)/gi, "/:token");
1917
+ pathname = pathname.replace(/\/\d+(?=\/|$)/g, "/:id");
1918
+ let queryString = "";
1919
+ if (parsed.search) {
1920
+ const params = new URLSearchParams(parsed.search);
1921
+ const redacted = new URLSearchParams();
1922
+ for (const [key] of params) {
1923
+ redacted.set(key, "<redacted>");
1924
+ }
1925
+ queryString = "?" + redacted.toString();
1926
+ }
1927
+ return `${pathname}${queryString}`;
1928
+ } catch {
1929
+ return url;
1930
+ }
1931
+ }
1932
+ var PATH_ANCHOR_HOST = "piwi.invalid";
1933
+ function pageKey(url) {
1934
+ const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(url);
1935
+ if (hasScheme && !/^https?:\/\//i.test(url)) return null;
1936
+ const absolute = /^https?:\/\//i.test(url) ? url : `http://${PATH_ANCHOR_HOST}${url.startsWith("/") ? "" : "/"}${url}`;
1937
+ const normalized = normalizeRoute(absolute);
1938
+ const q = normalized.indexOf("?");
1939
+ return q < 0 ? normalized : normalized.slice(0, q);
1940
+ }
1941
+
1942
+ // src/internal/capture/locator-pages.ts
1943
+ var MAX_LOCATOR_PAGE_ENTRIES = 1e3;
1944
+ var MAX_LOCATOR_CHARS = 1e3;
1945
+ var NON_INTERACTIONS = /* @__PURE__ */ new Set(["waitFor"]);
1946
+ var interactions = /* @__PURE__ */ new WeakMap();
1947
+ function noteNavigation(page) {
1948
+ interactions.set(page, 0);
1949
+ }
1950
+ function isOnArrival(page) {
1951
+ return (interactions.get(page) ?? 0) === 0;
1952
+ }
1953
+ function noteLocatorCall(page, method, assertion = false) {
1954
+ const arrival = isOnArrival(page);
1955
+ if (!assertion && !NON_INTERACTIONS.has(method)) interactions.set(page, (interactions.get(page) ?? 0) + 1);
1956
+ return arrival;
1957
+ }
1958
+ function pageOf(url) {
1959
+ const key = pageKey(url);
1960
+ if (key === null) return null;
1961
+ try {
1962
+ const origin = new URL(url).origin;
1963
+ return origin && origin !== "null" ? { origin, page: key } : null;
1964
+ } catch {
1965
+ return null;
1966
+ }
1967
+ }
1968
+ var LocatorPageLog = class {
1969
+ constructor() {
1970
+ this.entries = /* @__PURE__ */ new Map();
1971
+ }
1972
+ /** Record one call. Silently skipped without a call site, off an http(s) page, or past the limits. */
1973
+ record(call) {
1974
+ if (!call.location || !call.locator || call.locator.length > MAX_LOCATOR_CHARS) return;
1975
+ const where = pageOf(call.url);
1976
+ if (!where) return;
1977
+ const key = `${call.location}\0${call.locator}\0${where.origin}\0${where.page}`;
1978
+ const existing = this.entries.get(key);
1979
+ if (existing) {
1980
+ existing.arrival ||= call.arrival;
1981
+ return;
1982
+ }
1983
+ if (this.entries.size >= MAX_LOCATOR_PAGE_ENTRIES) return;
1984
+ this.entries.set(key, { location: call.location, locator: call.locator, ...where, arrival: call.arrival });
1985
+ }
1986
+ get size() {
1987
+ return this.entries.size;
1988
+ }
1989
+ /** The entries in the order the calls first happened. */
1990
+ list() {
1991
+ return [...this.entries.values()];
1992
+ }
1993
+ };
1994
+
1872
1995
  // src/internal/probe/mode.ts
1873
1996
  var fs2 = __toESM(require("fs"));
1874
1997
  var path3 = __toESM(require("path"));
@@ -1987,8 +2110,8 @@ function parseRouteKey(routeKey) {
1987
2110
  return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
1988
2111
  }
1989
2112
  function patternToRegex(pattern) {
1990
- const path6 = pattern.split("?")[0] ?? pattern;
1991
- const escaped = path6.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2113
+ const path8 = pattern.split("?")[0] ?? pattern;
2114
+ const escaped = path8.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
1992
2115
  return new RegExp(`^${escaped}$`);
1993
2116
  }
1994
2117
  function suitePathEqual(a, b) {
@@ -2011,13 +2134,13 @@ function matchProbeItem(plan, test) {
2011
2134
  function requestMatchesRoute(method, url, routeKey) {
2012
2135
  const { method: rm, pattern } = parseRouteKey(routeKey);
2013
2136
  if (rm && rm !== method.toUpperCase()) return false;
2014
- let path6;
2137
+ let path8;
2015
2138
  try {
2016
- path6 = new URL(url).pathname;
2139
+ path8 = new URL(url).pathname;
2017
2140
  } catch {
2018
- path6 = url.split("?")[0] ?? url;
2141
+ path8 = url.split("?")[0] ?? url;
2019
2142
  }
2020
- return patternToRegex(pattern).test(path6);
2143
+ return patternToRegex(pattern).test(path8);
2021
2144
  }
2022
2145
  function createProbeState() {
2023
2146
  return { navigated: false, matches: 0 };
@@ -2039,10 +2162,10 @@ function isProbeMode() {
2039
2162
  function loadPlan() {
2040
2163
  if (planCache !== void 0) return planCache;
2041
2164
  planCache = null;
2042
- const path6 = process.env[PIWI_PROBE_ENV.plan];
2043
- if (path6) {
2165
+ const path8 = process.env[PIWI_PROBE_ENV.plan];
2166
+ if (path8) {
2044
2167
  try {
2045
- planCache = JSON.parse(fs2.readFileSync(path6, "utf8"));
2168
+ planCache = JSON.parse(fs2.readFileSync(path8, "utf8"));
2046
2169
  } catch {
2047
2170
  planCache = null;
2048
2171
  }
@@ -2064,10 +2187,10 @@ function outcomeFromStatus(status, applied) {
2064
2187
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2065
2188
  }
2066
2189
  function recordProbeOutcome(line) {
2067
- const path6 = process.env[PIWI_PROBE_ENV.results];
2068
- if (!path6) return;
2190
+ const path8 = process.env[PIWI_PROBE_ENV.results];
2191
+ if (!path8) return;
2069
2192
  try {
2070
- fs2.appendFileSync(path6, `${JSON.stringify(line)}
2193
+ fs2.appendFileSync(path8, `${JSON.stringify(line)}
2071
2194
  `);
2072
2195
  } catch {
2073
2196
  }
@@ -2249,7 +2372,7 @@ async function installProbeInterception(page, item) {
2249
2372
  });
2250
2373
  applied = out.changed;
2251
2374
  if (applied) appliedFault = item.fault;
2252
- if (out.delayMs > 0) await new Promise((resolve) => setTimeout(resolve, out.delayMs));
2375
+ if (out.delayMs > 0) await new Promise((resolve2) => setTimeout(resolve2, out.delayMs));
2253
2376
  const headers = fulfillHeaders(response.headers(), out.contentType);
2254
2377
  await route.fulfill({ status: out.status, body: out.body, headers });
2255
2378
  } catch {
@@ -2791,6 +2914,79 @@ function isDueForAriaSample(testInfo) {
2791
2914
  return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
2792
2915
  }
2793
2916
 
2917
+ // src/internal/capture/quiet-capture.ts
2918
+ var import_node_module = require("module");
2919
+ var path7 = __toESM(require("path"));
2920
+
2921
+ // src/internal/support/reporter-version.ts
2922
+ var fs4 = __toESM(require("fs"));
2923
+ var path6 = __toESM(require("path"));
2924
+ var CANDIDATE_OFFSETS = ["..", "../../.."];
2925
+ function findOwnPackageJson(fromDir) {
2926
+ for (const offset of CANDIDATE_OFFSETS) {
2927
+ try {
2928
+ const pkgPath = path6.resolve(fromDir, offset, "package.json");
2929
+ const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
2930
+ const { name, version } = pkg;
2931
+ if (name === "@piwitests/reporter" && typeof version === "string")
2932
+ return { version, root: path6.dirname(pkgPath) };
2933
+ } catch {
2934
+ }
2935
+ }
2936
+ return void 0;
2937
+ }
2938
+
2939
+ // src/internal/capture/quiet-capture.ts
2940
+ var BOXED_PREFIX_EXPORTS = [
2941
+ [
2942
+ "playwright-core/lib/coreBundle",
2943
+ (mod) => mod?.utils?.setBoxedStackPrefixes
2944
+ ],
2945
+ ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
2946
+ ];
2947
+ var boxing;
2948
+ function resolveBoxing() {
2949
+ try {
2950
+ const root = findOwnPackageJson(__dirname)?.root;
2951
+ if (!root) return null;
2952
+ const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
2953
+ const playwrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
2954
+ const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
2955
+ for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
2956
+ let set;
2957
+ try {
2958
+ set = pick(requireFromPlaywright(moduleId));
2959
+ } catch {
2960
+ continue;
2961
+ }
2962
+ if (typeof set !== "function") continue;
2963
+ return {
2964
+ set,
2965
+ prefixes: [path7.dirname(playwrightPackageJson), path7.join(root, "dist") + path7.sep]
2966
+ };
2967
+ }
2968
+ } catch {
2969
+ }
2970
+ return null;
2971
+ }
2972
+ function boxCaptureFrames(extraPrefixes = []) {
2973
+ const first = boxing === void 0;
2974
+ if (first) boxing = resolveBoxing();
2975
+ if (!boxing) return;
2976
+ const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
2977
+ if (!first && added.length === 0) return;
2978
+ boxing.prefixes.push(...added);
2979
+ try {
2980
+ boxing.set([...boxing.prefixes]);
2981
+ } catch {
2982
+ }
2983
+ }
2984
+ function internalCall(owner, run) {
2985
+ const wrap = owner?._wrapApiCall;
2986
+ if (typeof wrap !== "function") return run();
2987
+ return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
2988
+ }
2989
+
2794
2990
  // src/internal/capture/capture-fixtures.ts
2795
2991
  function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
2796
2992
  const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
@@ -2841,7 +3037,8 @@ function createSink() {
2841
3037
  probeItem: null,
2842
3038
  probeInterception: null,
2843
3039
  pickOffered: false,
2844
- userPick: null
3040
+ userPick: null,
3041
+ locatorPages: new LocatorPageLog()
2845
3042
  };
2846
3043
  }
2847
3044
  var currentSink = null;
@@ -2851,8 +3048,8 @@ async function drainPendingProbes(capMs) {
2851
3048
  let cap;
2852
3049
  await Promise.race([
2853
3050
  Promise.allSettled(PENDING_PROBES),
2854
- new Promise((resolve) => {
2855
- cap = setTimeout(resolve, capMs);
3051
+ new Promise((resolve2) => {
3052
+ cap = setTimeout(resolve2, capMs);
2856
3053
  })
2857
3054
  ]);
2858
3055
  clearTimeout(cap);
@@ -2892,10 +3089,10 @@ async function readWebVitals(page) {
2892
3089
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
2893
3090
  paint[key] = Math.round(entry.startTime);
2894
3091
  }
2895
- const readBuffered = (type, extra) => new Promise((resolve) => {
3092
+ const readBuffered = (type, extra) => new Promise((resolve2) => {
2896
3093
  try {
2897
3094
  const PO = globalThis.PerformanceObserver;
2898
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve(null);
3095
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve2(null);
2899
3096
  const out = [];
2900
3097
  const po = new PO((list) => out.push(...list.getEntries()));
2901
3098
  po.observe({ type, buffered: true, ...extra });
@@ -2905,10 +3102,10 @@ async function readWebVitals(page) {
2905
3102
  po.disconnect();
2906
3103
  } catch {
2907
3104
  }
2908
- resolve(out);
3105
+ resolve2(out);
2909
3106
  }, 0);
2910
3107
  } catch {
2911
- resolve(null);
3108
+ resolve2(null);
2912
3109
  }
2913
3110
  });
2914
3111
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -3021,12 +3218,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
3021
3218
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
3022
3219
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
3023
3220
  function withTimeout(promise, ms) {
3024
- return new Promise((resolve, reject) => {
3221
+ return new Promise((resolve2, reject) => {
3025
3222
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
3026
3223
  promise.then(
3027
3224
  (value) => {
3028
3225
  clearTimeout(timer);
3029
- resolve(value);
3226
+ resolve2(value);
3030
3227
  },
3031
3228
  (error) => {
3032
3229
  clearTimeout(timer);
@@ -3060,7 +3257,7 @@ async function recordPageWindow(sink, page) {
3060
3257
  if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
3061
3258
  if (key && !inventoriedPageKeys.has(key)) {
3062
3259
  inventoriedPageKeys.add(key);
3063
- const content = await readPageInventory(page);
3260
+ const content = await internalCall(page, () => readPageInventory(page));
3064
3261
  if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
3065
3262
  sink.pageInventories.push({
3066
3263
  url: content?.url ?? url,
@@ -3077,6 +3274,9 @@ async function stashPageState(sink, closing) {
3077
3274
  if (!page || isPageClosed(page)) return;
3078
3275
  const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
3079
3276
  if (!belongsToClosing) return;
3277
+ await internalCall(page, () => readPageBeforeClose(sink, page));
3278
+ }
3279
+ async function readPageBeforeClose(sink, page) {
3080
3280
  const vitals = await readWebVitals(page);
3081
3281
  if (vitals) sink.stashedWebVitals = vitals;
3082
3282
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
@@ -3118,7 +3318,10 @@ async function maybeOpenPicker(sink, closing) {
3118
3318
  console.log("[piwi] locator picker: no failing locator could be identified in this failure \u2014 nothing to replace.");
3119
3319
  return;
3120
3320
  }
3121
- const pick = await runLocatorPicker(page, testInfo, failed, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG });
3321
+ const pick = await internalCall(
3322
+ page,
3323
+ () => runLocatorPicker(page, testInfo, failed, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG })
3324
+ );
3122
3325
  if (!pick) return;
3123
3326
  sink.userPick = pick;
3124
3327
  applyPickToSnapshots(sink.capturedLocators, pick);
@@ -3137,12 +3340,8 @@ var CAPTURED_ATTRS_ARG = {
3137
3340
  // special-cased logic in the probe, so add them explicitly).
3138
3341
  roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
3139
3342
  // The reporter always wants ancestor-anchored alternatives (the picker's
3140
- // anchors step and generateAnchoredAlternatives both need them). `labelText`
3141
- // is what names a form field, and the probe reads it from `el.labels` in the
3142
- // same pass — so asking for it here is free and settles the accessible name
3143
- // of a labeled field without a second round trip (`exactAccessibleName`).
3144
- includeStructural: true,
3145
- includeLabelText: true
3343
+ // anchors step and generateAnchoredAlternatives both need them).
3344
+ includeStructural: true
3146
3345
  };
3147
3346
  var PROBE_GLOBAL = "__piwiProbeElement";
3148
3347
  var PROBE_INIT_SCRIPT = `(() => {
@@ -3153,15 +3352,18 @@ var PROBE_INIT_SCRIPT = `(() => {
3153
3352
  var PROBE_UNSEEDED_PAGES = /* @__PURE__ */ new WeakSet();
3154
3353
  function probeElement(page, target) {
3155
3354
  const shipSource = () => target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
3156
- if (!page || PROBE_UNSEEDED_PAGES.has(page)) return shipSource();
3157
- return target.evaluate((el, name) => {
3158
- const seeded = globalThis[name];
3159
- return typeof seeded === "function" ? seeded(el) : null;
3160
- }, PROBE_GLOBAL).then((attrs) => {
3161
- if (attrs) return attrs;
3162
- PROBE_UNSEEDED_PAGES.add(page);
3163
- return shipSource();
3164
- });
3355
+ if (!page || PROBE_UNSEEDED_PAGES.has(page)) return internalCall(page, shipSource);
3356
+ return internalCall(
3357
+ page,
3358
+ () => target.evaluate((el, name) => {
3359
+ const seeded = globalThis[name];
3360
+ return typeof seeded === "function" ? seeded(el) : null;
3361
+ }, PROBE_GLOBAL).then((attrs) => {
3362
+ if (attrs) return attrs;
3363
+ PROBE_UNSEEDED_PAGES.add(page);
3364
+ return shipSource();
3365
+ })
3366
+ );
3165
3367
  }
3166
3368
  async function ariaSnapshotBestEffort(target, timeout) {
3167
3369
  if (typeof target.ariaSnapshot !== "function") return null;
@@ -3212,7 +3414,7 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
3212
3414
  const role = resolveAriaRole({ ...attrs, accessibleName: null });
3213
3415
  const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
3214
3416
  const exactName = exactAccessibleName(attrs, role);
3215
- const aria = exactName === null && (role || isFormField) ? await ariaSnapshotBestEffort(target, 500) : null;
3417
+ const aria = exactName === null && (role || isFormField) ? await internalCall(page, () => ariaSnapshotBestEffort(target, 500)) : null;
3216
3418
  const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
3217
3419
  sink.capturedLocators[seq] = {
3218
3420
  location: callerLocation,
@@ -3240,15 +3442,23 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
3240
3442
  })();
3241
3443
  sink.capturePromises.push(resolveAttrs);
3242
3444
  }
3445
+ function callMethod(target, prop, args) {
3446
+ return target[prop](...args);
3447
+ }
3448
+ function recordLocatorPage(sink, page, locator, location, arrival) {
3449
+ try {
3450
+ sink.locatorPages.record({ location, locator: String(locator), url: page.url(), arrival });
3451
+ } catch {
3452
+ }
3453
+ }
3243
3454
  function wrapLocator(page, locator, originMethod, originArgs) {
3244
3455
  return new Proxy(locator, {
3245
3456
  get(target, prop) {
3246
3457
  const original = Reflect.get(target, prop);
3247
3458
  if (typeof original !== "function") return original;
3248
- const fn = original;
3249
3459
  if (CHAIN_METHOD_SET.has(prop)) {
3250
3460
  return (...args) => {
3251
- const next = fn.apply(target, args);
3461
+ const next = callMethod(target, prop, args);
3252
3462
  if (LOCATOR_CREATING_CHAINS.has(prop)) {
3253
3463
  return wrapLocator(page, next, String(prop), args);
3254
3464
  }
@@ -3260,11 +3470,12 @@ function wrapLocator(page, locator, originMethod, originArgs) {
3260
3470
  const sink = currentSink;
3261
3471
  const expression = typeof callArgs[0] === "string" ? callArgs[0] : "";
3262
3472
  const isNot = Boolean(callArgs[1]?.isNot);
3473
+ const callerLocation = sink ? captureCallerLocation() : null;
3474
+ if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
3263
3475
  if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
3264
- return fn.apply(target, callArgs);
3476
+ return callMethod(target, prop, callArgs);
3265
3477
  }
3266
3478
  sink.lastActivePage = page;
3267
- const callerLocation = captureCallerLocation();
3268
3479
  const used = {
3269
3480
  method: originMethod,
3270
3481
  args: originArgs,
@@ -3276,7 +3487,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
3276
3487
  seq = sink.capturedLocators.length;
3277
3488
  sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
3278
3489
  }
3279
- const result = await fn.apply(target, callArgs);
3490
+ const result = await callMethod(target, prop, callArgs);
3280
3491
  const matches = result?.matches;
3281
3492
  if (matches === true && !alreadyProbed) {
3282
3493
  if (callerLocation) sink.probedLocations.add(callerLocation);
@@ -3290,9 +3501,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
3290
3501
  if (!ACTION_METHOD_SET.has(prop)) return original;
3291
3502
  return async (...callArgs) => {
3292
3503
  const sink = currentSink;
3293
- if (!sink) return fn.apply(target, callArgs);
3504
+ if (!sink) return callMethod(target, prop, callArgs);
3294
3505
  sink.lastActivePage = page;
3295
3506
  const callerLocation = captureCallerLocation();
3507
+ recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, String(prop)));
3296
3508
  const used = {
3297
3509
  method: originMethod,
3298
3510
  args: originArgs,
@@ -3311,7 +3523,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
3311
3523
  }
3312
3524
  let result;
3313
3525
  try {
3314
- result = await fn.apply(target, callArgs);
3526
+ result = await callMethod(target, prop, callArgs);
3315
3527
  } catch (error) {
3316
3528
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
3317
3529
  throw error;
@@ -3330,11 +3542,10 @@ function wrapFrameLocator(page, frameLocator) {
3330
3542
  get(target, prop) {
3331
3543
  const original = Reflect.get(target, prop);
3332
3544
  if (typeof original !== "function") return original;
3333
- const fn = original;
3334
3545
  if (!LOCATOR_METHOD_SET.has(prop)) return original;
3335
3546
  return (...args) => {
3336
3547
  if (currentSink) currentSink.lastActivePage = page;
3337
- return wrapLocator(page, fn.apply(target, args), String(prop), args);
3548
+ return wrapLocator(page, callMethod(target, prop, args), String(prop), args);
3338
3549
  };
3339
3550
  }
3340
3551
  });
@@ -3381,7 +3592,13 @@ function instrumentPage(page) {
3381
3592
  };
3382
3593
  }
3383
3594
  if (typeof page.on === "function") {
3384
- page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
3595
+ page.on("framenavigated", (frame) => {
3596
+ PROBE_UNSEEDED_PAGES.delete(page);
3597
+ try {
3598
+ if (frame === page.mainFrame()) noteNavigation(page);
3599
+ } catch {
3600
+ }
3601
+ });
3385
3602
  }
3386
3603
  }
3387
3604
  if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
@@ -3520,13 +3737,19 @@ function patchBrowser(browser) {
3520
3737
  };
3521
3738
  }
3522
3739
  }
3740
+ function readRootAria(page) {
3741
+ return internalCall(page, () => ariaSnapshotBestEffort(page.locator(":root")));
3742
+ }
3743
+ function readRootAriaJson(page) {
3744
+ return internalCall(page, () => ariaSnapshotJSONBestEffort(page.locator(":root")));
3745
+ }
3523
3746
  async function flushSink(sink, testInfo) {
3524
3747
  await Promise.allSettled(sink.pendingHandlers);
3525
3748
  let drainDeadline;
3526
3749
  await Promise.race([
3527
3750
  Promise.allSettled(sink.capturePromises),
3528
- new Promise((resolve) => {
3529
- drainDeadline = setTimeout(resolve, 2e3);
3751
+ new Promise((resolve2) => {
3752
+ drainDeadline = setTimeout(resolve2, 2e3);
3530
3753
  })
3531
3754
  ]);
3532
3755
  clearTimeout(drainDeadline);
@@ -3542,15 +3765,21 @@ async function flushSink(sink, testInfo) {
3542
3765
  body: Buffer.from(JSON.stringify(dedupeSnapshotsByLocation(sink.capturedLocators)))
3543
3766
  });
3544
3767
  }
3768
+ if (sink.locatorPages.size > 0) {
3769
+ await testInfo.attach(ATTACHMENT_NAMES.locatorPages, {
3770
+ contentType: "application/json",
3771
+ body: Buffer.from(JSON.stringify(sink.locatorPages.list()))
3772
+ });
3773
+ }
3545
3774
  if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
3546
3775
  try {
3547
- const snapshot = (pageReadable ? await ariaSnapshotBestEffort(page.locator(":root")) : null) ?? sink.stashedAria;
3776
+ const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
3548
3777
  if (snapshot) {
3549
3778
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
3550
3779
  contentType: "text/plain",
3551
3780
  body: snapshot
3552
3781
  });
3553
- const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
3782
+ const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
3554
3783
  if (snapshotJson) {
3555
3784
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
3556
3785
  contentType: "application/json",
@@ -3575,13 +3804,13 @@ async function flushSink(sink, testInfo) {
3575
3804
  }
3576
3805
  if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
3577
3806
  try {
3578
- const snapshot = (pageReadable ? await ariaSnapshotBestEffort(page.locator(":root")) : null) ?? sink.stashedAria;
3807
+ const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
3579
3808
  if (snapshot) {
3580
3809
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
3581
3810
  contentType: "text/plain",
3582
3811
  body: snapshot
3583
3812
  });
3584
- const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
3813
+ const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
3585
3814
  if (snapshotJson) {
3586
3815
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
3587
3816
  contentType: "application/json",
@@ -3627,7 +3856,7 @@ async function flushSink(sink, testInfo) {
3627
3856
  body: Buffer.from(JSON.stringify(sink.networkRequests))
3628
3857
  });
3629
3858
  }
3630
- const webVitals = (pageReadable ? await readWebVitals(page) : null) ?? sink.stashedWebVitals;
3859
+ const webVitals = (pageReadable ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
3631
3860
  if (webVitals) {
3632
3861
  await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
3633
3862
  contentType: "application/json",
@@ -3635,7 +3864,7 @@ async function flushSink(sink, testInfo) {
3635
3864
  });
3636
3865
  }
3637
3866
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
3638
- const pageState = (pageReadable ? await readPageState(page) : null) ?? sink.stashedPageState;
3867
+ const pageState = (pageReadable ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
3639
3868
  if (pageState) {
3640
3869
  await testInfo.attach(ATTACHMENT_NAMES.pageState, {
3641
3870
  contentType: "application/json",
@@ -3676,6 +3905,7 @@ var piwiFixtures = {
3676
3905
  // requested, so suites that never destructure `page` are still captured.
3677
3906
  piwiCapture: [
3678
3907
  async ({}, use, testInfo) => {
3908
+ boxCaptureFrames();
3679
3909
  const sink = createSink();
3680
3910
  sink.testInfo = testInfo;
3681
3911
  if (isProbeMode())
@@ -3720,11 +3950,17 @@ function extendPiwiFixtures(test) {
3720
3950
  // Annotate the CommonJS export names for ESM import in node:
3721
3951
  0 && (module.exports = {
3722
3952
  CAPTURED_ATTRS_ARG,
3953
+ LocatorPageLog,
3723
3954
  ariaSnapshotBestEffort,
3724
3955
  ariaSnapshotJSONBestEffort,
3956
+ boxCaptureFrames,
3725
3957
  buildPageState,
3958
+ callMethod,
3726
3959
  computeCoreVitals,
3727
3960
  extendPiwiFixtures,
3961
+ internalCall,
3962
+ noteLocatorCall,
3963
+ noteNavigation,
3728
3964
  piwiFixtures,
3729
3965
  probeElementAttrs
3730
3966
  });