@piwitests/reporter 0.39.0 → 0.41.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,20 +31,56 @@ 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,
40
+ codeReachRoots: () => codeReachRoots,
37
41
  computeCoreVitals: () => computeCoreVitals,
42
+ configuredCodeReachRoots: () => configuredCodeReachRoots,
38
43
  extendPiwiFixtures: () => extendPiwiFixtures,
44
+ internalCall: () => internalCall,
45
+ noteLocatorCall: () => noteLocatorCall,
46
+ noteNavigation: () => noteNavigation,
47
+ pageMapFetcher: () => pageMapFetcher,
39
48
  piwiFixtures: () => piwiFixtures,
40
- probeElementAttrs: () => probeElementAttrs
49
+ probeElementAttrs: () => probeElementAttrs,
50
+ resolveCodeReach: () => resolveCodeReach,
51
+ startCodeReach: () => startCodeReach,
52
+ stopCodeReach: () => stopCodeReach
41
53
  });
42
54
  module.exports = __toCommonJS(capture_fixtures_exports);
55
+ var path10 = __toESM(require("path"));
43
56
  var import_node_zlib2 = require("zlib");
44
57
 
45
58
  // ../picker-dom/src/probe.ts
46
59
  function probeElementAttrs(el, arg) {
47
- const { keep, tagRoles, inputRoles, roleSources, includeStructural, includeLabelText } = arg;
60
+ const { keep, tagRoles, inputRoles, roleSources, includeStructural } = arg;
61
+ const FIELD_TAGS = ["input", "select", "textarea"];
62
+ const textWithout = (root, skip) => {
63
+ if (root === skip) return "";
64
+ if (root.nodeType === 3) return root.nodeValue || "";
65
+ let text = "";
66
+ const kids = root.childNodes || [];
67
+ for (let i = 0; i < kids.length; i++) text += textWithout(kids[i], skip);
68
+ return text;
69
+ };
70
+ const labelTextOf = (n) => {
71
+ try {
72
+ const ids = (n.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
73
+ let text = "";
74
+ for (const id of ids) {
75
+ const ref = n.ownerDocument.getElementById(id);
76
+ if (ref) text += ` ${textWithout(ref, n)}`;
77
+ }
78
+ if (!text.trim() && n.labels && n.labels.length > 0) text = textWithout(n.labels[0], n);
79
+ return text.replace(/\s+/g, " ").trim().slice(0, 120) || null;
80
+ } catch {
81
+ return null;
82
+ }
83
+ };
48
84
  const attrMap = {};
49
85
  for (const key of keep) {
50
86
  const v = el.getAttribute(key) ?? el[key];
@@ -124,10 +160,18 @@ function probeElementAttrs(el, arg) {
124
160
  const targetRole = roleOf(el);
125
161
  const targetLevel = targetRole === "heading" ? levelOf(el) : null;
126
162
  const nameOf = (n) => {
163
+ const labelledBy = !!n.getAttribute("aria-labelledby");
164
+ const isField = FIELD_TAGS.indexOf((n.tagName || "").toLowerCase()) !== -1;
165
+ const lt = labelledBy || isField ? labelTextOf(n) : null;
166
+ if (lt && labelledBy) return lt;
127
167
  const al = n.getAttribute("aria-label");
128
168
  if (al) return al;
129
- const txt = (n.textContent || "").replace(/\s+/g, " ").trim();
130
- if (txt) return txt;
169
+ if (isField) {
170
+ if (lt) return lt;
171
+ } else {
172
+ const txt = (n.textContent || "").replace(/\s+/g, " ").trim();
173
+ if (txt) return txt;
174
+ }
131
175
  return n.getAttribute("title") || n.getAttribute("placeholder") || null;
132
176
  };
133
177
  const targetName = nameOf(el);
@@ -319,16 +363,7 @@ function probeElementAttrs(el, arg) {
319
363
  }
320
364
  }
321
365
  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
- }
366
+ const labelText = labelTextOf(el);
332
367
  return {
333
368
  tagName: el.tagName?.toLowerCase?.() ?? "unknown",
334
369
  attributes: attrMap,
@@ -340,7 +375,7 @@ function probeElementAttrs(el, arg) {
340
375
  y: Math.round(r.y + r.height / 2)
341
376
  },
342
377
  hasLabel,
343
- ...includeLabelText ? { labelText } : {},
378
+ labelText,
344
379
  selectorCounts,
345
380
  ...includeStructural ? { rolePosition, ancestors } : {}
346
381
  };
@@ -493,6 +528,11 @@ function installPickerOverlay(arg) {
493
528
  placeLabel(r);
494
529
  foot.textContent = "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip";
495
530
  };
531
+ const redescribe = () => {
532
+ labeled = null;
533
+ if (current()) refresh();
534
+ };
535
+ g.__piwiRedescribe = redescribe;
496
536
  const stop = (e) => {
497
537
  e.preventDefault();
498
538
  e.stopImmediatePropagation();
@@ -547,6 +587,7 @@ function installPickerOverlay(arg) {
547
587
  highlight.remove();
548
588
  label.remove();
549
589
  banner.remove();
590
+ if (g.__piwiRedescribe === redescribe) delete g.__piwiRedescribe;
550
591
  };
551
592
  const reportPicked = (el) => {
552
593
  removePickingListeners();
@@ -1093,8 +1134,9 @@ function isAutoGenerated(value) {
1093
1134
  }
1094
1135
  if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
1095
1136
  if (value.startsWith("ng-")) return true;
1096
- if (/^(radix-|headlessui-|mui-|mantine-|chakra-)/i.test(value)) return true;
1137
+ if (/^(radix-|headlessui-|mui-|mantine-|chakra-|reka-|bits-|downshift-|react-aria|el-id-)/i.test(value)) return true;
1097
1138
  if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
1139
+ if (/(^|[-_:])v-\d+(-\d+)+$/.test(value) || /(^|[-_:])v-\d+(-\d+)+[-_:]/.test(value)) return true;
1098
1140
  return false;
1099
1141
  }
1100
1142
  var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
@@ -1311,9 +1353,15 @@ function generateAlternatives(attrs) {
1311
1353
  }
1312
1354
  function approximateAccessibleName(attrs) {
1313
1355
  const a = attrs.attributes;
1356
+ const labelText = attrs.labelText || null;
1357
+ if (a["aria-labelledby"] && labelText) return labelText;
1314
1358
  const ariaLabel = a["aria-label"];
1315
1359
  if (ariaLabel) return ariaLabel;
1316
- if (attrs.textContent) return attrs.textContent;
1360
+ if (["input", "select", "textarea"].includes(attrs.tagName)) {
1361
+ if (labelText) return labelText;
1362
+ } else if (attrs.textContent) {
1363
+ return attrs.textContent;
1364
+ }
1317
1365
  const title = a["title"];
1318
1366
  if (title) return title;
1319
1367
  const placeholder = a["placeholder"];
@@ -1743,6 +1791,8 @@ var ATTACHMENT_NAMES = {
1743
1791
  dialogs: "piwi-dialogs",
1744
1792
  network: "piwi-network",
1745
1793
  pageInventory: "piwi-page-inventory",
1794
+ locatorPages: "piwi-locator-pages",
1795
+ codeReach: "piwi-code-reach",
1746
1796
  webVitals: "piwi-web-vitals",
1747
1797
  locatorSuggestion: "piwi-locator-suggestion",
1748
1798
  pageState: "piwi-page-state",
@@ -1869,6 +1919,99 @@ function inventoryPageKey(url) {
1869
1919
  }
1870
1920
  }
1871
1921
 
1922
+ // ../core/src/page-key.ts
1923
+ function normalizeRoute(url) {
1924
+ try {
1925
+ const parsed = new URL(url);
1926
+ let pathname = parsed.pathname;
1927
+ pathname = pathname.replace(/\/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?=\/|$)/g, "/:jwt");
1928
+ 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");
1929
+ pathname = pathname.replace(/\/[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}(?=\/|$)/gi, "/:ulid");
1930
+ pathname = pathname.replace(/\/[A-Za-z0-9_-]{40,}(?=\/|$)/g, "/:token");
1931
+ pathname = pathname.replace(/\/[0-9a-f]{32,}(?=\/|$)/gi, "/:token");
1932
+ pathname = pathname.replace(/\/\d+(?=\/|$)/g, "/:id");
1933
+ let queryString = "";
1934
+ if (parsed.search) {
1935
+ const params = new URLSearchParams(parsed.search);
1936
+ const redacted = new URLSearchParams();
1937
+ for (const [key] of params) {
1938
+ redacted.set(key, "<redacted>");
1939
+ }
1940
+ queryString = "?" + redacted.toString();
1941
+ }
1942
+ return `${pathname}${queryString}`;
1943
+ } catch {
1944
+ return url;
1945
+ }
1946
+ }
1947
+ var PATH_ANCHOR_HOST = "piwi.invalid";
1948
+ function pageKey(url) {
1949
+ const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(url);
1950
+ if (hasScheme && !/^https?:\/\//i.test(url)) return null;
1951
+ const absolute = /^https?:\/\//i.test(url) ? url : `http://${PATH_ANCHOR_HOST}${url.startsWith("/") ? "" : "/"}${url}`;
1952
+ const normalized = normalizeRoute(absolute);
1953
+ const q = normalized.indexOf("?");
1954
+ return q < 0 ? normalized : normalized.slice(0, q);
1955
+ }
1956
+ function requestRouteKey(method, url) {
1957
+ const route = normalizeRoute(url ?? "");
1958
+ const q = route.indexOf("?");
1959
+ return `${(method || "GET").toUpperCase()} ${q === -1 ? route : route.slice(0, q)}`;
1960
+ }
1961
+
1962
+ // src/internal/capture/locator-pages.ts
1963
+ var MAX_LOCATOR_PAGE_ENTRIES = 1e3;
1964
+ var MAX_LOCATOR_CHARS = 1e3;
1965
+ var NON_INTERACTIONS = /* @__PURE__ */ new Set(["waitFor"]);
1966
+ var interactions = /* @__PURE__ */ new WeakMap();
1967
+ function noteNavigation(page) {
1968
+ interactions.set(page, 0);
1969
+ }
1970
+ function isOnArrival(page) {
1971
+ return (interactions.get(page) ?? 0) === 0;
1972
+ }
1973
+ function noteLocatorCall(page, method, assertion = false) {
1974
+ const arrival = isOnArrival(page);
1975
+ if (!assertion && !NON_INTERACTIONS.has(method)) interactions.set(page, (interactions.get(page) ?? 0) + 1);
1976
+ return arrival;
1977
+ }
1978
+ function pageOf(url) {
1979
+ const key = pageKey(url);
1980
+ if (key === null) return null;
1981
+ try {
1982
+ const origin = new URL(url).origin;
1983
+ return origin && origin !== "null" ? { origin, page: key } : null;
1984
+ } catch {
1985
+ return null;
1986
+ }
1987
+ }
1988
+ var LocatorPageLog = class {
1989
+ constructor() {
1990
+ this.entries = /* @__PURE__ */ new Map();
1991
+ }
1992
+ /** Record one call. Silently skipped without a call site, off an http(s) page, or past the limits. */
1993
+ record(call) {
1994
+ if (!call.location || !call.locator || call.locator.length > MAX_LOCATOR_CHARS) return;
1995
+ const where = pageOf(call.url);
1996
+ if (!where) return;
1997
+ const key = `${call.location}\0${call.locator}\0${where.origin}\0${where.page}`;
1998
+ const existing = this.entries.get(key);
1999
+ if (existing) {
2000
+ existing.arrival ||= call.arrival;
2001
+ return;
2002
+ }
2003
+ if (this.entries.size >= MAX_LOCATOR_PAGE_ENTRIES) return;
2004
+ this.entries.set(key, { location: call.location, locator: call.locator, ...where, arrival: call.arrival });
2005
+ }
2006
+ get size() {
2007
+ return this.entries.size;
2008
+ }
2009
+ /** The entries in the order the calls first happened. */
2010
+ list() {
2011
+ return [...this.entries.values()];
2012
+ }
2013
+ };
2014
+
1872
2015
  // src/internal/probe/mode.ts
1873
2016
  var fs2 = __toESM(require("fs"));
1874
2017
  var path3 = __toESM(require("path"));
@@ -1892,6 +2035,8 @@ var DEFAULTS = {
1892
2035
  // Off by default: the page inventory reads control and link names from every
1893
2036
  // visited page, so it stays opt-in until a project turns it on.
1894
2037
  capturePageInventory: false,
2038
+ // Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
2039
+ captureCodeReach: false,
1895
2040
  captureServerTraces: true,
1896
2041
  sampleAriaOnPass: true,
1897
2042
  uploadManifest: true,
@@ -1928,6 +2073,8 @@ var PIWI_ENV_KEYS = {
1928
2073
  captureLocators: "PIWI_CAPTURE_LOCATORS",
1929
2074
  capturePageState: "PIWI_CAPTURE_PAGE_STATE",
1930
2075
  capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
2076
+ captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
2077
+ codeReachRoots: "PIWI_CODE_REACH_ROOTS",
1931
2078
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
1932
2079
  sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
1933
2080
  uploadManifest: "PIWI_UPLOAD_MANIFEST",
@@ -1950,6 +2097,10 @@ var PIWI_PROBE_ENV = {
1950
2097
  results: "PIWI_PROBE_RESULTS",
1951
2098
  secret: "PIWI_PROBE_SECRET"
1952
2099
  };
2100
+ var PIWI_FLAKE_ENV = {
2101
+ plan: "PIWI_FLAKE_PLAN",
2102
+ results: "PIWI_FLAKE_RESULTS"
2103
+ };
1953
2104
  var ENV_FALLBACK_SPECS = [
1954
2105
  { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
1955
2106
  { option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
@@ -1971,6 +2122,7 @@ var ENV_FALLBACK_SPECS = [
1971
2122
  { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
1972
2123
  { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
1973
2124
  { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
2125
+ { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
1974
2126
  { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
1975
2127
  { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
1976
2128
  { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
@@ -1981,14 +2133,14 @@ var ENV_FALLBACK_SPECS = [
1981
2133
  ];
1982
2134
 
1983
2135
  // src/internal/probe/plan.ts
1984
- function parseRouteKey(routeKey) {
1985
- const space = routeKey.indexOf(" ");
1986
- if (space < 0) return { method: "", pattern: routeKey };
1987
- return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
2136
+ function parseRouteKey(routeKey2) {
2137
+ const space = routeKey2.indexOf(" ");
2138
+ if (space < 0) return { method: "", pattern: routeKey2 };
2139
+ return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
1988
2140
  }
1989
2141
  function patternToRegex(pattern) {
1990
- const path6 = pattern.split("?")[0] ?? pattern;
1991
- const escaped = path6.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2142
+ const path11 = pattern.split("?")[0] ?? pattern;
2143
+ const escaped = path11.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
1992
2144
  return new RegExp(`^${escaped}$`);
1993
2145
  }
1994
2146
  function suitePathEqual(a, b) {
@@ -2008,16 +2160,16 @@ function matchProbeItem(plan, test) {
2008
2160
  }
2009
2161
  return null;
2010
2162
  }
2011
- function requestMatchesRoute(method, url, routeKey) {
2012
- const { method: rm, pattern } = parseRouteKey(routeKey);
2163
+ function requestMatchesRoute(method, url, routeKey2) {
2164
+ const { method: rm, pattern } = parseRouteKey(routeKey2);
2013
2165
  if (rm && rm !== method.toUpperCase()) return false;
2014
- let path6;
2166
+ let path11;
2015
2167
  try {
2016
- path6 = new URL(url).pathname;
2168
+ path11 = new URL(url).pathname;
2017
2169
  } catch {
2018
- path6 = url.split("?")[0] ?? url;
2170
+ path11 = url.split("?")[0] ?? url;
2019
2171
  }
2020
- return patternToRegex(pattern).test(path6);
2172
+ return patternToRegex(pattern).test(path11);
2021
2173
  }
2022
2174
  function createProbeState() {
2023
2175
  return { navigated: false, matches: 0 };
@@ -2025,10 +2177,10 @@ function createProbeState() {
2025
2177
  function markNavigated(state) {
2026
2178
  state.navigated = true;
2027
2179
  }
2028
- function shouldMutate(state, item) {
2180
+ function shouldAct(state, match) {
2029
2181
  if (!state.navigated) return false;
2030
2182
  state.matches += 1;
2031
- return state.matches === Math.max(1, item.nth ?? 1);
2183
+ return match === "all" || state.matches === Math.max(1, match);
2032
2184
  }
2033
2185
 
2034
2186
  // src/internal/probe/mode.ts
@@ -2039,10 +2191,10 @@ function isProbeMode() {
2039
2191
  function loadPlan() {
2040
2192
  if (planCache !== void 0) return planCache;
2041
2193
  planCache = null;
2042
- const path6 = process.env[PIWI_PROBE_ENV.plan];
2043
- if (path6) {
2194
+ const path11 = process.env[PIWI_PROBE_ENV.plan];
2195
+ if (path11) {
2044
2196
  try {
2045
- planCache = JSON.parse(fs2.readFileSync(path6, "utf8"));
2197
+ planCache = JSON.parse(fs2.readFileSync(path11, "utf8"));
2046
2198
  } catch {
2047
2199
  planCache = null;
2048
2200
  }
@@ -2064,10 +2216,10 @@ function outcomeFromStatus(status, applied) {
2064
2216
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2065
2217
  }
2066
2218
  function recordProbeOutcome(line) {
2067
- const path6 = process.env[PIWI_PROBE_ENV.results];
2068
- if (!path6) return;
2219
+ const path11 = process.env[PIWI_PROBE_ENV.results];
2220
+ if (!path11) return;
2069
2221
  try {
2070
- fs2.appendFileSync(path6, `${JSON.stringify(line)}
2222
+ fs2.appendFileSync(path11, `${JSON.stringify(line)}
2071
2223
  `);
2072
2224
  } catch {
2073
2225
  }
@@ -2094,6 +2246,9 @@ function isFaultAllowed(level, fault) {
2094
2246
  return list.includes(fault);
2095
2247
  }
2096
2248
  var SLOW_FAULT_DELAY_MS = 5e3;
2249
+ function faultRouteAction(fault) {
2250
+ return fault === "slow" ? { kind: "delay", ms: SLOW_FAULT_DELAY_MS } : null;
2251
+ }
2097
2252
  function dropOneField(body) {
2098
2253
  let parsed;
2099
2254
  try {
@@ -2194,11 +2349,46 @@ function originOf(url) {
2194
2349
  return null;
2195
2350
  }
2196
2351
  }
2352
+ var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, Math.max(0, ms)));
2353
+ async function performRouteAction(route, action, startedAt) {
2354
+ switch (action.kind) {
2355
+ case "abort":
2356
+ await route.abort("connectionreset");
2357
+ return;
2358
+ case "status":
2359
+ await route.fulfill({ status: action.status, body: "" });
2360
+ return;
2361
+ case "delay": {
2362
+ const response = await route.fetch();
2363
+ await sleep(startedAt + action.ms - Date.now());
2364
+ await route.fulfill({ response });
2365
+ return;
2366
+ }
2367
+ }
2368
+ }
2369
+ async function installRouteRules(page, rules) {
2370
+ const state = rules.map(() => createProbeState());
2371
+ page.on("framenavigated", (frame) => {
2372
+ if (frame === page.mainFrame()) for (const s of state) markNavigated(s);
2373
+ });
2374
+ await page.route("**/*", async (route) => {
2375
+ const startedAt = Date.now();
2376
+ const request = route.request();
2377
+ for (let i = 0; i < rules.length; i++) {
2378
+ const rule = rules[i];
2379
+ if (rule.done?.() || !rule.matches(request.method(), request.url())) continue;
2380
+ if (!shouldAct(state[i], rule.match)) continue;
2381
+ await rule.handle(route, startedAt);
2382
+ return;
2383
+ }
2384
+ await route.fallback();
2385
+ });
2386
+ return { state };
2387
+ }
2197
2388
  async function installProbeInterception(page, item) {
2198
2389
  if (!isFaultAllowed(item.level ?? "client", item.fault)) {
2199
2390
  return { applied: () => false, appliedFault: () => null, serverError: () => false };
2200
2391
  }
2201
- const state = createProbeState();
2202
2392
  let applied = false;
2203
2393
  let appliedFault = null;
2204
2394
  let serverError = false;
@@ -2206,17 +2396,12 @@ async function installProbeInterception(page, item) {
2206
2396
  const probeSecret = process.env[PIWI_PROBE_ENV.secret];
2207
2397
  page.on("framenavigated", (frame) => {
2208
2398
  if (frame === page.mainFrame()) {
2209
- markNavigated(state);
2210
2399
  const origin = originOf(page.url());
2211
2400
  if (origin) appOrigin = origin;
2212
2401
  }
2213
2402
  });
2214
- await page.route("**/*", async (route) => {
2403
+ const handle = async (route, startedAt) => {
2215
2404
  const request = route.request();
2216
- if (applied || !requestMatchesRoute(request.method(), request.url(), item.routeKey) || !shouldMutate(state, item)) {
2217
- await route.fallback();
2218
- return;
2219
- }
2220
2405
  if (item.level === "server") {
2221
2406
  if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
2222
2407
  await route.fallback();
@@ -2240,6 +2425,13 @@ async function installProbeInterception(page, item) {
2240
2425
  return;
2241
2426
  }
2242
2427
  try {
2428
+ const action = faultRouteAction(item.fault);
2429
+ if (action) {
2430
+ applied = true;
2431
+ appliedFault = item.fault;
2432
+ await performRouteAction(route, action, startedAt);
2433
+ return;
2434
+ }
2243
2435
  const response = await route.fetch();
2244
2436
  const body = await response.text();
2245
2437
  const out = computeFault(item.fault, {
@@ -2249,53 +2441,26 @@ async function installProbeInterception(page, item) {
2249
2441
  });
2250
2442
  applied = out.changed;
2251
2443
  if (applied) appliedFault = item.fault;
2252
- if (out.delayMs > 0) await new Promise((resolve) => setTimeout(resolve, out.delayMs));
2253
2444
  const headers = fulfillHeaders(response.headers(), out.contentType);
2254
2445
  await route.fulfill({ status: out.status, body: out.body, headers });
2255
2446
  } catch {
2256
2447
  applied = false;
2257
2448
  await route.fallback();
2258
2449
  }
2259
- });
2260
- return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
2261
- }
2262
-
2263
- // src/internal/capture/inspect-on-failure.ts
2264
- function isCi(ci) {
2265
- return ci !== void 0 && ci !== "" && ci !== "false";
2266
- }
2267
- function shouldInspectOnFailure(gate) {
2268
- if (gate.enabled !== "true") return false;
2269
- if (isCi(gate.ci)) return false;
2270
- if (gate.headless !== false) return false;
2271
- if (gate.status !== "failed" && gate.status !== "timedOut") return false;
2272
- if (gate.status === gate.expectedStatus) return false;
2273
- return gate.retry >= gate.retries;
2274
- }
2275
- function environmentalSkipReason(gate) {
2276
- if (gate.enabled !== "true") return null;
2277
- if (gate.status !== "failed" && gate.status !== "timedOut") return null;
2278
- if (gate.status === gate.expectedStatus) return null;
2279
- if (isCi(gate.ci)) return "running under CI \u2014 this is a headed, local-only feature";
2280
- if (gate.headless !== false) {
2281
- return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
2282
- }
2283
- return null;
2284
- }
2285
- function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT_ON_FAIL) {
2286
- const use = testInfo.project?.use ?? {};
2287
- return {
2288
- enabled,
2289
- ci: process.env.CI,
2290
- status: testInfo.status,
2291
- expectedStatus: testInfo.expectedStatus,
2292
- headless: use.headless,
2293
- retry: testInfo.retry,
2294
- retries: testInfo.project?.retries ?? 0
2295
2450
  };
2451
+ await installRouteRules(page, [
2452
+ {
2453
+ matches: (method, url) => requestMatchesRoute(method, url, item.routeKey),
2454
+ match: Math.max(1, item.nth ?? 1),
2455
+ handle,
2456
+ done: () => applied
2457
+ }
2458
+ ]);
2459
+ return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
2296
2460
  }
2297
2461
 
2298
- // src/internal/capture/pick-on-failure.ts
2462
+ // src/internal/flake/mode.ts
2463
+ var fs3 = __toESM(require("fs"));
2299
2464
  var path4 = __toESM(require("path"));
2300
2465
 
2301
2466
  // ../core/src/locator-chain.ts
@@ -2580,8 +2745,474 @@ function locatorCallValues(call, opts = {}) {
2580
2745
  return call.args.map(value).filter((v) => v !== void 0);
2581
2746
  }
2582
2747
 
2748
+ // ../core/src/error-parse.ts
2749
+ var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
2750
+ function stripAnsi(text) {
2751
+ return text.replace(ANSI_RE, "");
2752
+ }
2753
+ var SELECTOR_FN_RE = /\b(?:locator|frameLocator|getByRole|getByTestId|getByText|getByLabel|getByPlaceholder|getByAltText|getByTitle)\(/;
2754
+ var CHAIN_LINK_METHODS = /* @__PURE__ */ new Set([
2755
+ ...LOCATOR_BUILDER_METHODS,
2756
+ "frameLocator",
2757
+ "contentFrame",
2758
+ "filter",
2759
+ "first",
2760
+ "last",
2761
+ "nth",
2762
+ "and",
2763
+ "or",
2764
+ "describe"
2765
+ ]);
2766
+ function extractMessageHead(text) {
2767
+ let head = text;
2768
+ const callLogIdx = head.indexOf("\nCall log:");
2769
+ if (callLogIdx !== -1) head = head.slice(0, callLogIdx);
2770
+ const stackIdx = head.search(/\n\s+at /);
2771
+ if (stackIdx !== -1) head = head.slice(0, stackIdx);
2772
+ const lines = head.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
2773
+ return lines.slice(0, 5).join("\n");
2774
+ }
2775
+ function extractSelector(text) {
2776
+ const match = SELECTOR_FN_RE.exec(text);
2777
+ if (!match) return null;
2778
+ const start = match.index;
2779
+ let depth = 0;
2780
+ for (let i = start; i < Math.min(text.length, start + 200); i++) {
2781
+ const ch = text[i];
2782
+ if (ch === "(") {
2783
+ depth++;
2784
+ } else if (ch === ")") {
2785
+ depth--;
2786
+ if (depth === 0) return text.slice(start, i + 1);
2787
+ } else if (ch === "\n") {
2788
+ break;
2789
+ }
2790
+ }
2791
+ return text.slice(start, start + 80);
2792
+ }
2793
+ var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
2794
+ function extractTopFrame(text) {
2795
+ const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
2796
+ let m;
2797
+ while ((m = frameRe.exec(text)) !== null) {
2798
+ const file = m[1].replace(/\\/g, "/");
2799
+ if (file.includes("node_modules") || file.startsWith("node:")) continue;
2800
+ return { file, line: Number(m[2]), column: Number(m[3]) };
2801
+ }
2802
+ return null;
2803
+ }
2804
+ function extractTopFrameFile(text) {
2805
+ return extractTopFrame(text)?.file ?? null;
2806
+ }
2807
+ var SUBJECTS = "locator|page|frame|frameLocator|elementHandle|mouse|keyboard|touchscreen|browserContext|browser|apiRequestContext|request|response|route|download|dialog|fileChooser|expect|worker|jsHandle|clock|tracing|video";
2808
+ var CALL_RE = new RegExp(`\\b(${SUBJECTS})\\.(\\w+): `);
2809
+
2810
+ // ../core/src/error-signature.ts
2811
+ var UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
2812
+ var LONG_HEX_RE = /\b[0-9a-f]{8,}\b/gi;
2813
+ var SHORT_HEX_RE = /\b(?=[0-9a-f]*[a-f])(?=[0-9a-f]*[0-9])[0-9a-f]{6,7}\b/gi;
2814
+ var URL_RE = /\bhttps?:\/\/[^\s'"`)]+/gi;
2815
+ var EMAIL_RE = /\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b/gi;
2816
+ function classifyError(text) {
2817
+ if (/strict mode violation/i.test(text)) return "strict-mode";
2818
+ if (/\bexpect\(|\bexpect\.|Expected (?:string|substring|pattern|value)|\.toHave|\.toBe|\.toContain|\.toEqual/.test(text))
2819
+ return "assertion";
2820
+ if (/Target page, context or browser has been closed|Target closed|browser has been closed|Page crashed/i.test(text))
2821
+ return "crash";
2822
+ if (/net::ERR_|NS_ERROR_|Navigation failed/i.test(text)) return "navigation";
2823
+ if (/Timeout \d+m?s exceeded|TimeoutError|Timed out \d+m?s/i.test(text)) return "timeout";
2824
+ return "unknown";
2825
+ }
2826
+ function maskVolatile(text) {
2827
+ return text.replace(/^(\s*(?:Received|Expected)[^:\n]*:).*$/gm, "$1 <VALUE>").replace(URL_RE, "<URL>").replace(EMAIL_RE, "<EMAIL>").replace(UUID_RE, "<UUID>").replace(LONG_HEX_RE, "<HASH>").replace(SHORT_HEX_RE, "<HASH>").replace(/([A-Za-z])?(\d+)/g, (whole, letter) => letter ? whole : "<N>");
2828
+ }
2829
+ function extractErrorSignature(rawError) {
2830
+ const text = stripAnsi(rawError);
2831
+ const errorType = classifyError(text);
2832
+ const normalizedMessage = maskVolatile(extractMessageHead(text));
2833
+ const selector = extractSelector(text);
2834
+ const topFrameFile = extractTopFrameFile(text);
2835
+ const signature = (normalizedMessage.split("\n")[0] || "").slice(0, 200) || "Unknown error";
2836
+ return { errorType, signature, normalizedMessage, selector, topFrameFile };
2837
+ }
2838
+
2839
+ // ../core/src/flake-plan.ts
2840
+ var FLAKE_PLAN_VERSION = 1;
2841
+ var FLAKE_CONDITION_KINDS = [
2842
+ "delay",
2843
+ "fail",
2844
+ "cpu",
2845
+ "network",
2846
+ "alongside",
2847
+ "after",
2848
+ "project"
2849
+ ];
2850
+ var COMMAND_CONDITION_KINDS = ["alongside", "after", "project"];
2851
+ function flakeErrorSignature(rawError) {
2852
+ return extractErrorSignature(rawError).normalizedMessage;
2853
+ }
2854
+ var FlakePlanError = class extends Error {
2855
+ constructor(message) {
2856
+ super(`Invalid flake plan: ${message}`);
2857
+ this.name = "FlakePlanError";
2858
+ }
2859
+ };
2860
+ function isObject(value) {
2861
+ return !!value && typeof value === "object" && !Array.isArray(value);
2862
+ }
2863
+ function str(obj, key, at) {
2864
+ const value = obj[key];
2865
+ if (typeof value !== "string" || !value.trim()) throw new FlakePlanError(`${at}.${key} must be a non-empty string`);
2866
+ return value;
2867
+ }
2868
+ function num(obj, key, at, min, max = Number.MAX_SAFE_INTEGER) {
2869
+ const value = obj[key];
2870
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
2871
+ throw new FlakePlanError(`${at}.${key} must be a number from ${min} to ${max}`);
2872
+ }
2873
+ return value;
2874
+ }
2875
+ function stringList(value, at) {
2876
+ if (value === void 0) return [];
2877
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
2878
+ throw new FlakePlanError(`${at} must be a list of strings`);
2879
+ }
2880
+ return value;
2881
+ }
2882
+ var ROUTE_KEY_RE = /^[A-Z]+ \/\S*$/;
2883
+ function routeKey(obj, at) {
2884
+ const route = str(obj, "route", at);
2885
+ if (!ROUTE_KEY_RE.test(route)) {
2886
+ throw new FlakePlanError(`${at}.route must be a method and a path, like "GET /api/cart/:id" (got "${route}")`);
2887
+ }
2888
+ return route;
2889
+ }
2890
+ function routeMatch(obj, at) {
2891
+ const value = obj.match;
2892
+ if (value === void 0 || value === "all") return "all";
2893
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
2894
+ throw new FlakePlanError(`${at}.match must be "all" or a whole number from 1`);
2895
+ }
2896
+ function testRef(value, at) {
2897
+ if (!isObject(value)) throw new FlakePlanError(`${at} must be an object`);
2898
+ return {
2899
+ file: str(value, "file", at),
2900
+ title: str(value, "title", at),
2901
+ suite: stringList(value.suite, `${at}.suite`)
2902
+ };
2903
+ }
2904
+ function parseCondition(value, at) {
2905
+ if (!isObject(value)) throw new FlakePlanError(`${at} must be an object`);
2906
+ const kind = value.kind;
2907
+ switch (kind) {
2908
+ case "delay":
2909
+ return { kind, route: routeKey(value, at), ms: num(value, "ms", at, 0, 6e5), match: routeMatch(value, at) };
2910
+ case "fail": {
2911
+ const route = routeKey(value, at);
2912
+ const match = routeMatch(value, at);
2913
+ if (value.abort === true) {
2914
+ if (value.status !== void 0) throw new FlakePlanError(`${at} sets both status and abort; choose one`);
2915
+ return { kind, route, abort: true, match };
2916
+ }
2917
+ const status = num(value, "status", at, 100, 599);
2918
+ if (!Number.isInteger(status)) throw new FlakePlanError(`${at}.status must be a whole number`);
2919
+ return { kind, route, status, match };
2920
+ }
2921
+ case "cpu":
2922
+ return { kind, rate: num(value, "rate", at, 1, 100) };
2923
+ case "network":
2924
+ return {
2925
+ kind,
2926
+ latencyMs: num(value, "latencyMs", at, 0, 6e4),
2927
+ downKbps: num(value, "downKbps", at, 1),
2928
+ upKbps: num(value, "upKbps", at, 1)
2929
+ };
2930
+ case "alongside":
2931
+ case "after":
2932
+ return { kind, test: testRef(value.test, `${at}.test`) };
2933
+ case "project":
2934
+ return { kind, name: str(value, "name", at) };
2935
+ default:
2936
+ throw new FlakePlanError(
2937
+ `${at}.kind ${JSON.stringify(kind)} is not a condition this version knows (${FLAKE_CONDITION_KINDS.join(", ")})`
2938
+ );
2939
+ }
2940
+ }
2941
+ function parseFlakePlan(value) {
2942
+ if (!isObject(value)) throw new FlakePlanError("the plan must be a JSON object");
2943
+ if (value.version !== FLAKE_PLAN_VERSION) {
2944
+ throw new FlakePlanError(`version must be ${FLAKE_PLAN_VERSION} (got ${JSON.stringify(value.version)})`);
2945
+ }
2946
+ const experimentId = str(value, "experimentId", "plan");
2947
+ if (!isObject(value.test)) throw new FlakePlanError("plan.test must be an object");
2948
+ const ref = testRef(value.test, "plan.test");
2949
+ const project = value.test.project;
2950
+ if (project !== void 0 && project !== null && (typeof project !== "string" || !project.trim())) {
2951
+ throw new FlakePlanError("plan.test.project must be a non-empty string or null");
2952
+ }
2953
+ if (!isObject(value.arm)) throw new FlakePlanError("plan.arm must be an object");
2954
+ const armId = str(value.arm, "id", "plan.arm");
2955
+ const rawConditions = value.arm.conditions ?? [];
2956
+ if (!Array.isArray(rawConditions)) throw new FlakePlanError("plan.arm.conditions must be a list");
2957
+ const conditions = rawConditions.map((c, i) => parseCondition(c, `plan.arm.conditions[${i}]`));
2958
+ return {
2959
+ version: FLAKE_PLAN_VERSION,
2960
+ experimentId,
2961
+ test: { ...ref, project: typeof project === "string" ? project : null },
2962
+ arm: { id: armId, conditions },
2963
+ errorSignatures: stringList(value.errorSignatures, "plan.errorSignatures")
2964
+ };
2965
+ }
2966
+ function parseFlakePlanText(text) {
2967
+ let json;
2968
+ try {
2969
+ json = JSON.parse(text);
2970
+ } catch (error) {
2971
+ throw new FlakePlanError(`not valid JSON (${error instanceof Error ? error.message : String(error)})`);
2972
+ }
2973
+ return parseFlakePlan(json);
2974
+ }
2975
+ function suiteEqual(a, b) {
2976
+ return !!b && a.length === b.length && a.every((title, i) => title === b[i]);
2977
+ }
2978
+ function flakeTestMatches(ref, test) {
2979
+ if (!test.file || ref.file !== test.file || ref.title !== test.title) return false;
2980
+ return test.suite === void 0 || ref.suite.length === 0 || suiteEqual(ref.suite, test.suite);
2981
+ }
2982
+
2983
+ // src/internal/support/errors.ts
2984
+ function errorMessage(error) {
2985
+ if (error instanceof Error) return error.message;
2986
+ return String(error);
2987
+ }
2988
+
2989
+ // src/internal/flake/mode.ts
2990
+ function isFlakeMode() {
2991
+ return !!process.env[PIWI_FLAKE_ENV.plan]?.trim();
2992
+ }
2993
+ function labModeConflict() {
2994
+ if (!isProbeMode() || !isFlakeMode()) return null;
2995
+ return `[Piwi Dashboard] ${PIWI_PROBE_ENV.flag} and ${PIWI_FLAKE_ENV.plan} are both set: a run is a probe run or a flake-lab run, not both. Unset one of them.`;
2996
+ }
2997
+ var planCache2 = null;
2998
+ function loadFlakePlan() {
2999
+ const file = process.env[PIWI_FLAKE_ENV.plan]?.trim() ?? "";
3000
+ if (planCache2 && planCache2.file === file) return planCache2.plan;
3001
+ let text;
3002
+ try {
3003
+ text = fs3.readFileSync(file, "utf8");
3004
+ } catch (error) {
3005
+ throw new Error(`[Piwi Dashboard] Cannot read the flake plan ${file}: ${errorMessage(error)}`);
3006
+ }
3007
+ try {
3008
+ const plan = parseFlakePlanText(text);
3009
+ planCache2 = { file, plan };
3010
+ return plan;
3011
+ } catch (error) {
3012
+ throw new Error(`[Piwi Dashboard] ${errorMessage(error)} (${file})`);
3013
+ }
3014
+ }
3015
+ function relativeTestFile(file) {
3016
+ return file ? path4.relative(process.cwd(), file).split(path4.sep).join("/") : null;
3017
+ }
3018
+ function flakeRoleForTest(plan, test) {
3019
+ const running = {
3020
+ file: relativeTestFile(test.file),
3021
+ title: test.title,
3022
+ suite: test.titlePath ? test.titlePath.slice(1, -1) : void 0
3023
+ };
3024
+ if (flakeTestMatches(plan.test, running) && (plan.test.project === null || plan.test.project === test.project)) {
3025
+ return "target";
3026
+ }
3027
+ const companion = plan.arm.conditions.some(
3028
+ (c) => (c.kind === "alongside" || c.kind === "after") && flakeTestMatches(c.test, running)
3029
+ );
3030
+ return companion ? "companion" : null;
3031
+ }
3032
+ function pageBrowserName(page) {
3033
+ try {
3034
+ return page.context().browser()?.browserType().name() ?? null;
3035
+ } catch {
3036
+ return null;
3037
+ }
3038
+ }
3039
+ function cdpCommandFor(condition) {
3040
+ if (condition.kind === "cpu") return { method: "Emulation.setCPUThrottlingRate", params: { rate: condition.rate } };
3041
+ if (condition.kind === "network") {
3042
+ return {
3043
+ method: "Network.emulateNetworkConditions",
3044
+ params: {
3045
+ offline: false,
3046
+ latency: condition.latencyMs,
3047
+ // Kilobits per second to bytes per second.
3048
+ downloadThroughput: condition.downKbps * 1e3 / 8,
3049
+ uploadThroughput: condition.upKbps * 1e3 / 8
3050
+ }
3051
+ };
3052
+ }
3053
+ return null;
3054
+ }
3055
+ function conditionRouteAction(condition) {
3056
+ if (condition.kind === "delay") return { kind: "delay", ms: condition.ms };
3057
+ if (condition.kind === "fail")
3058
+ return "abort" in condition ? { kind: "abort" } : { kind: "status", status: condition.status };
3059
+ return null;
3060
+ }
3061
+ function unappliedReports(plan) {
3062
+ return plan.arm.conditions.map((c) => {
3063
+ if (COMMAND_CONDITION_KINDS.includes(c.kind)) return { kind: c.kind, outcome: "by-command" };
3064
+ if (cdpCommandFor(c)) return { kind: c.kind, outcome: "skipped", note: "the attempt opened no page" };
3065
+ return { kind: c.kind, outcome: "not-matched" };
3066
+ });
3067
+ }
3068
+ async function installFlakeConditions(page, plan) {
3069
+ const conditions = plan.arm.conditions;
3070
+ const reports = unappliedReports(plan);
3071
+ const rules = [];
3072
+ conditions.forEach((condition, i) => {
3073
+ const action = conditionRouteAction(condition);
3074
+ if (!action || condition.kind !== "delay" && condition.kind !== "fail") return;
3075
+ const route = condition.route;
3076
+ rules.push({
3077
+ matches: (method, url) => requestRouteKey(method, url) === route,
3078
+ match: condition.match,
3079
+ handle: async (r, startedAt) => {
3080
+ reports[i] = { kind: condition.kind, outcome: "applied" };
3081
+ try {
3082
+ await performRouteAction(r, action, startedAt);
3083
+ } catch {
3084
+ await r.fallback().catch(() => {
3085
+ });
3086
+ }
3087
+ }
3088
+ });
3089
+ });
3090
+ await installRouteRules(page, rules);
3091
+ const cdp = conditions.map((c, i) => ({ i, command: cdpCommandFor(c) })).filter((c) => c.command);
3092
+ if (cdp.length > 0) {
3093
+ const browserName = pageBrowserName(page);
3094
+ if (browserName !== null && browserName !== "chromium") {
3095
+ for (const { i } of cdp) {
3096
+ reports[i] = {
3097
+ kind: conditions[i].kind,
3098
+ outcome: "skipped",
3099
+ note: `needs Chromium; this attempt ran in ${browserName}`
3100
+ };
3101
+ }
3102
+ } else {
3103
+ try {
3104
+ const session = await page.context().newCDPSession(page);
3105
+ for (const { i, command } of cdp) {
3106
+ try {
3107
+ if (command.method.startsWith("Network.")) await session.send("Network.enable");
3108
+ await session.send(command.method, command.params);
3109
+ reports[i] = { kind: conditions[i].kind, outcome: "applied" };
3110
+ } catch (error) {
3111
+ reports[i] = { kind: conditions[i].kind, outcome: "skipped", note: errorMessage(error) };
3112
+ }
3113
+ }
3114
+ } catch (error) {
3115
+ for (const { i } of cdp) {
3116
+ reports[i] = {
3117
+ kind: conditions[i].kind,
3118
+ outcome: "skipped",
3119
+ note: `no DevTools session: ${errorMessage(error)}`
3120
+ };
3121
+ }
3122
+ }
3123
+ }
3124
+ }
3125
+ return { reports: () => reports.map((r) => ({ ...r })) };
3126
+ }
3127
+ var FAILED_STATUSES = /* @__PURE__ */ new Set(["failed", "timedOut", "interrupted"]);
3128
+ function flakeResultLine(plan, attempt) {
3129
+ const failed = FAILED_STATUSES.has(attempt.status);
3130
+ const errorSignature = failed && attempt.errorText ? flakeErrorSignature(attempt.errorText) : null;
3131
+ return {
3132
+ version: FLAKE_PLAN_VERSION,
3133
+ experimentId: plan.experimentId,
3134
+ armId: plan.arm.id,
3135
+ role: attempt.role,
3136
+ file: relativeTestFile(attempt.file),
3137
+ title: attempt.title,
3138
+ project: attempt.project,
3139
+ browserName: attempt.browserName,
3140
+ status: attempt.status,
3141
+ errorSignature,
3142
+ matchesHistory: errorSignature !== null && plan.errorSignatures.includes(errorSignature),
3143
+ startedAt: attempt.startedAt,
3144
+ duration: attempt.duration,
3145
+ workerIndex: attempt.workerIndex,
3146
+ parallelIndex: attempt.parallelIndex,
3147
+ repeatEachIndex: attempt.repeatEachIndex,
3148
+ retry: attempt.retry,
3149
+ conditions: attempt.role === "target" ? attempt.conditions : []
3150
+ };
3151
+ }
3152
+ function recordFlakeResult(line) {
3153
+ const file = process.env[PIWI_FLAKE_ENV.results]?.trim();
3154
+ if (!file) return;
3155
+ try {
3156
+ fs3.appendFileSync(file, `${JSON.stringify(line)}
3157
+ `);
3158
+ } catch {
3159
+ }
3160
+ }
3161
+
3162
+ // ../core/src/error-text.ts
3163
+ function joinErrorMessages(errors) {
3164
+ if (!errors || errors.length === 0) return null;
3165
+ const seen = /* @__PURE__ */ new Set();
3166
+ const parts = [];
3167
+ for (const err of errors) {
3168
+ const msg = err.message ?? "";
3169
+ if (!msg) continue;
3170
+ const dedupeKey = msg.trim().slice(0, 200);
3171
+ if (seen.has(dedupeKey)) continue;
3172
+ seen.add(dedupeKey);
3173
+ parts.push(msg);
3174
+ }
3175
+ return parts.join("\n---\n") || null;
3176
+ }
3177
+
3178
+ // src/internal/capture/inspect-on-failure.ts
3179
+ function isCi(ci) {
3180
+ return ci !== void 0 && ci !== "" && ci !== "false";
3181
+ }
3182
+ function shouldInspectOnFailure(gate) {
3183
+ if (gate.enabled !== "true") return false;
3184
+ if (isCi(gate.ci)) return false;
3185
+ if (gate.headless !== false) return false;
3186
+ if (gate.status !== "failed" && gate.status !== "timedOut") return false;
3187
+ if (gate.status === gate.expectedStatus) return false;
3188
+ return gate.retry >= gate.retries;
3189
+ }
3190
+ function environmentalSkipReason(gate) {
3191
+ if (gate.enabled !== "true") return null;
3192
+ if (gate.status !== "failed" && gate.status !== "timedOut") return null;
3193
+ if (gate.status === gate.expectedStatus) return null;
3194
+ if (isCi(gate.ci)) return "running under CI \u2014 this is a headed, local-only feature";
3195
+ if (gate.headless !== false) {
3196
+ return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
3197
+ }
3198
+ return null;
3199
+ }
3200
+ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT_ON_FAIL) {
3201
+ const use = testInfo.project?.use ?? {};
3202
+ return {
3203
+ enabled,
3204
+ ci: process.env.CI,
3205
+ status: testInfo.status,
3206
+ expectedStatus: testInfo.expectedStatus,
3207
+ headless: use.headless,
3208
+ retry: testInfo.retry,
3209
+ retries: testInfo.project?.retries ?? 0
3210
+ };
3211
+ }
3212
+
2583
3213
  // src/internal/capture/pick-on-failure.ts
2584
- var ANSI_RE = /\[[0-9;]*m/g;
3214
+ var path5 = __toESM(require("path"));
3215
+ var ANSI_RE2 = /\[[0-9;]*m/g;
2585
3216
  function parseLeafLocatorExpression(rawExpr) {
2586
3217
  const leaf = parseLeafLocatorCall(rawExpr);
2587
3218
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -2591,13 +3222,13 @@ function deriveFailedLocator(testInfo) {
2591
3222
  const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
2592
3223
  for (const err of errors) {
2593
3224
  const text = `${err.message ?? ""}
2594
- ${err.stack ?? ""}`.replace(ANSI_RE, "");
3225
+ ${err.stack ?? ""}`.replace(ANSI_RE2, "");
2595
3226
  const line = /^\s*Locator:\s*(.+)$/m.exec(text);
2596
3227
  if (!line) continue;
2597
3228
  const parsed = parseLeafLocatorExpression(line[1].trim());
2598
3229
  if (!parsed) continue;
2599
3230
  const loc = err.location;
2600
- const location = loc ? `${path4.relative(process.cwd(), loc.file).split(path4.sep).join("/")}:${loc.line}:${loc.column}` : null;
3231
+ const location = loc ? `${path5.relative(process.cwd(), loc.file).split(path5.sep).join("/")}:${loc.line}:${loc.column}` : null;
2601
3232
  return { method: parsed.method, args: parsed.args, location };
2602
3233
  }
2603
3234
  return null;
@@ -2746,9 +3377,9 @@ function applyPickToSnapshots(snapshots, pick) {
2746
3377
  }
2747
3378
 
2748
3379
  // src/internal/support/aria-sampling.ts
2749
- var path5 = __toESM(require("path"));
3380
+ var path6 = __toESM(require("path"));
2750
3381
  var os3 = __toESM(require("os"));
2751
- var fs3 = __toESM(require("fs"));
3382
+ var fs4 = __toESM(require("fs"));
2752
3383
 
2753
3384
  // src/internal/support/instance-id.ts
2754
3385
  var crypto = __toESM(require("crypto"));
@@ -2762,14 +3393,14 @@ function ariaSampleIdentity(filePath, title) {
2762
3393
  return `${filePath}\0${title}`;
2763
3394
  }
2764
3395
  function getAriaSampleFilePath(projectName) {
2765
- return path5.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
3396
+ return path6.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
2766
3397
  }
2767
3398
  var cachedSets = /* @__PURE__ */ new Map();
2768
3399
  function loadAriaSampleSet(projectName) {
2769
3400
  if (cachedSets.has(projectName)) return cachedSets.get(projectName);
2770
3401
  let set = null;
2771
3402
  try {
2772
- const raw = fs3.readFileSync(getAriaSampleFilePath(projectName), "utf8");
3403
+ const raw = fs4.readFileSync(getAriaSampleFilePath(projectName), "utf8");
2773
3404
  const parsed = JSON.parse(raw);
2774
3405
  if (parsed.projectName === projectName && Array.isArray(parsed.identities)) {
2775
3406
  set = new Set(parsed.identities.filter((x) => typeof x === "string"));
@@ -2780,15 +3411,525 @@ function loadAriaSampleSet(projectName) {
2780
3411
  cachedSets.set(projectName, set);
2781
3412
  return set;
2782
3413
  }
2783
- function relativeTestFile(file) {
2784
- return path5.relative(process.cwd(), file).split(path5.sep).join("/");
3414
+ function relativeTestFile2(file) {
3415
+ return path6.relative(process.cwd(), file).split(path6.sep).join("/");
2785
3416
  }
2786
3417
  function isDueForAriaSample(testInfo) {
2787
3418
  const projectName = process.env.PIWI_PROJECT_NAME;
2788
3419
  if (!projectName) return false;
2789
3420
  const set = loadAriaSampleSet(projectName);
2790
3421
  if (!set || set.size === 0) return false;
2791
- return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
3422
+ return set.has(ariaSampleIdentity(relativeTestFile2(testInfo.file), testInfo.title));
3423
+ }
3424
+
3425
+ // src/internal/capture/quiet-capture.ts
3426
+ var import_node_module = require("module");
3427
+ var path8 = __toESM(require("path"));
3428
+
3429
+ // src/internal/support/reporter-version.ts
3430
+ var fs5 = __toESM(require("fs"));
3431
+ var path7 = __toESM(require("path"));
3432
+ var CANDIDATE_OFFSETS = ["..", "../../.."];
3433
+ function findOwnPackageJson(fromDir) {
3434
+ for (const offset of CANDIDATE_OFFSETS) {
3435
+ try {
3436
+ const pkgPath = path7.resolve(fromDir, offset, "package.json");
3437
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
3438
+ const { name, version } = pkg;
3439
+ if (name === "@piwitests/reporter" && typeof version === "string")
3440
+ return { version, root: path7.dirname(pkgPath) };
3441
+ } catch {
3442
+ }
3443
+ }
3444
+ return void 0;
3445
+ }
3446
+
3447
+ // src/internal/capture/quiet-capture.ts
3448
+ var BOXED_PREFIX_EXPORTS = [
3449
+ [
3450
+ "playwright-core/lib/coreBundle",
3451
+ (mod) => mod?.utils?.setBoxedStackPrefixes
3452
+ ],
3453
+ ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
3454
+ ];
3455
+ var boxing;
3456
+ function resolveBoxing() {
3457
+ try {
3458
+ const root = findOwnPackageJson(__dirname)?.root;
3459
+ if (!root) return null;
3460
+ const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
3461
+ const playwrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
3462
+ const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
3463
+ for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
3464
+ let set;
3465
+ try {
3466
+ set = pick(requireFromPlaywright(moduleId));
3467
+ } catch {
3468
+ continue;
3469
+ }
3470
+ if (typeof set !== "function") continue;
3471
+ return {
3472
+ set,
3473
+ prefixes: [path8.dirname(playwrightPackageJson), path8.join(root, "dist") + path8.sep]
3474
+ };
3475
+ }
3476
+ } catch {
3477
+ }
3478
+ return null;
3479
+ }
3480
+ function boxCaptureFrames(extraPrefixes = []) {
3481
+ const first = boxing === void 0;
3482
+ if (first) boxing = resolveBoxing();
3483
+ if (!boxing) return;
3484
+ const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
3485
+ if (!first && added.length === 0) return;
3486
+ boxing.prefixes.push(...added);
3487
+ try {
3488
+ boxing.set([...boxing.prefixes]);
3489
+ } catch {
3490
+ }
3491
+ }
3492
+ function internalCall(owner, run) {
3493
+ const wrap = owner?._wrapApiCall;
3494
+ if (typeof wrap !== "function") return run();
3495
+ return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
3496
+ }
3497
+
3498
+ // src/internal/capture/code-reach.ts
3499
+ var crypto2 = __toESM(require("crypto"));
3500
+ var fs6 = __toESM(require("fs"));
3501
+ var path9 = __toESM(require("path"));
3502
+ var import_node_child_process = require("child_process");
3503
+
3504
+ // ../core/src/source-map.ts
3505
+ var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3506
+ var BASE64_VALUE = new Int8Array(128).fill(-1);
3507
+ for (let i = 0; i < BASE64.length; i++) BASE64_VALUE[BASE64.charCodeAt(i)] = i;
3508
+ function decodeMappings(mappings, sourceBase) {
3509
+ const lines = [];
3510
+ let current = { columns: [], sources: [] };
3511
+ let column = 0;
3512
+ let source = 0;
3513
+ const fields = [];
3514
+ let value = 0;
3515
+ let shift = 0;
3516
+ const flush = () => {
3517
+ if (fields.length === 0) return;
3518
+ column += fields[0];
3519
+ if (fields.length >= 4) {
3520
+ source += fields[1];
3521
+ current.columns.push(column);
3522
+ current.sources.push(sourceBase + source);
3523
+ } else {
3524
+ current.columns.push(column);
3525
+ current.sources.push(-1);
3526
+ }
3527
+ fields.length = 0;
3528
+ };
3529
+ for (let i = 0; i <= mappings.length; i++) {
3530
+ const ch = i < mappings.length ? mappings.charCodeAt(i) : 59;
3531
+ if (ch === 59 || ch === 44) {
3532
+ flush();
3533
+ if (ch === 59) {
3534
+ lines.push(current);
3535
+ current = { columns: [], sources: [] };
3536
+ column = 0;
3537
+ }
3538
+ continue;
3539
+ }
3540
+ const digit = ch < 128 ? BASE64_VALUE[ch] : -1;
3541
+ if (digit < 0) continue;
3542
+ value += (digit & 31) << shift;
3543
+ if (digit & 32) {
3544
+ shift += 5;
3545
+ continue;
3546
+ }
3547
+ fields.push(value & 1 ? -(value >>> 1) : value >>> 1);
3548
+ value = 0;
3549
+ shift = 0;
3550
+ }
3551
+ for (const line of lines) {
3552
+ if (line.columns.every((c, i) => i === 0 || c >= line.columns[i - 1])) continue;
3553
+ const order = line.columns.map((_, i) => i).sort((a, b) => line.columns[a] - line.columns[b]);
3554
+ line.columns = order.map((i) => line.columns[i]);
3555
+ line.sources = order.map((i) => line.sources[i]);
3556
+ }
3557
+ return lines;
3558
+ }
3559
+ function joinRoot(root, source) {
3560
+ if (!root || /^[a-z][\w+.-]*:/i.test(source) || source.startsWith("/")) return source;
3561
+ return root.endsWith("/") ? root + source : `${root}/${source}`;
3562
+ }
3563
+ function decodeSourceMap(raw) {
3564
+ const sources = [];
3565
+ const lines = [];
3566
+ const add = (map, lineOffset, columnOffset) => {
3567
+ if (map.sections) {
3568
+ for (const section of map.sections) {
3569
+ if (!add(section.map, section.offset.line, section.offset.column)) return false;
3570
+ }
3571
+ return true;
3572
+ }
3573
+ if (typeof map.mappings !== "string" || !Array.isArray(map.sources)) return false;
3574
+ const base = sources.length;
3575
+ for (const s of map.sources) sources.push(joinRoot(map.sourceRoot, s ?? ""));
3576
+ decodeMappings(map.mappings, base).forEach((segments, i) => {
3577
+ const at = lineOffset + i;
3578
+ const shifted = i === 0 && columnOffset ? segments.columns.map((c) => c + columnOffset) : segments.columns;
3579
+ const existing = lines[at];
3580
+ if (!existing) lines[at] = { columns: shifted, sources: segments.sources };
3581
+ else {
3582
+ existing.columns.push(...shifted);
3583
+ existing.sources.push(...segments.sources);
3584
+ }
3585
+ });
3586
+ return true;
3587
+ };
3588
+ if (raw.version !== void 0 && raw.version !== 3) return null;
3589
+ if (!add(raw, 0, 0)) return null;
3590
+ return {
3591
+ sources,
3592
+ sourceAt(line, column) {
3593
+ const segments = lines[line];
3594
+ if (!segments || segments.columns.length === 0) return null;
3595
+ let lo = 0;
3596
+ let hi = segments.columns.length - 1;
3597
+ if (segments.columns[0] > column) return null;
3598
+ while (lo < hi) {
3599
+ const mid = lo + hi + 1 >> 1;
3600
+ if (segments.columns[mid] <= column) lo = mid;
3601
+ else hi = mid - 1;
3602
+ }
3603
+ const source = segments.sources[lo];
3604
+ return source < 0 ? null : source;
3605
+ },
3606
+ mapsBetween(source, start, end) {
3607
+ for (let line = start.line; line <= end.line && line < lines.length; line++) {
3608
+ const segments = lines[line];
3609
+ if (!segments) continue;
3610
+ let i = 0;
3611
+ if (line === start.line) {
3612
+ let hi = segments.columns.length;
3613
+ while (i < hi) {
3614
+ const mid = i + hi >> 1;
3615
+ if (segments.columns[mid] < start.column) i = mid + 1;
3616
+ else hi = mid;
3617
+ }
3618
+ }
3619
+ for (; i < segments.columns.length; i++) {
3620
+ if (line === end.line && segments.columns[i] >= end.column) return false;
3621
+ if (segments.sources[i] === source) return true;
3622
+ }
3623
+ }
3624
+ return false;
3625
+ }
3626
+ };
3627
+ }
3628
+ function lineStarts(text) {
3629
+ const starts = [0];
3630
+ for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
3631
+ return starts;
3632
+ }
3633
+ function offsetToPosition(starts, offset) {
3634
+ let lo = 0;
3635
+ let hi = starts.length - 1;
3636
+ while (lo < hi) {
3637
+ const mid = lo + hi + 1 >> 1;
3638
+ if (starts[mid] <= offset) lo = mid;
3639
+ else hi = mid - 1;
3640
+ }
3641
+ return { line: lo, column: offset - starts[lo] };
3642
+ }
3643
+ function sourceMappingUrl(script) {
3644
+ const tail = script.slice(-4096);
3645
+ const matches = [...tail.matchAll(/(?:\/\/|\/\*)[#@]\s*sourceMappingURL=([^\s*'"]+)/g)];
3646
+ return matches.length ? matches[matches.length - 1][1] : null;
3647
+ }
3648
+ function decodeDataUrl(url) {
3649
+ const m = /^data:([^,]*?)(;base64)?,(.*)$/s.exec(url);
3650
+ if (!m) return null;
3651
+ try {
3652
+ if (!m[2]) return decodeURIComponent(m[3]);
3653
+ const binary = globalThis.atob(m[3]);
3654
+ const bytes = new Uint8Array(binary.length);
3655
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
3656
+ return new TextDecoder().decode(bytes);
3657
+ } catch {
3658
+ return null;
3659
+ }
3660
+ }
3661
+ function normalizeSourcePath(source) {
3662
+ let s = source.replace(/[?#].*$/, "");
3663
+ s = s.replace(/^webpack:\/\/[^/]*\//, "").replace(/^webpack:\/\//, "");
3664
+ s = s.replace(/^(?:vite|rollup|turbopack|ng):\/*/, "");
3665
+ s = s.replace(/^\[project\]\//, "");
3666
+ s = s.replace(/^file:\/\//, "");
3667
+ s = s.replace(/^\/@fs\//, "/");
3668
+ s = s.replace(/^\/@id\//, "");
3669
+ try {
3670
+ s = decodeURIComponent(s);
3671
+ } catch {
3672
+ }
3673
+ s = s.replace(/\\/g, "/");
3674
+ if (/^\/[A-Za-z]:\//.test(s)) s = s.slice(1);
3675
+ s = s.replace(/(^|\/)\.\//g, "$1");
3676
+ return s;
3677
+ }
3678
+
3679
+ // ../core/src/code-reach.ts
3680
+ var MAX_CODE_REACH_FILES = 2e3;
3681
+ function executedFunctionRanges(entry) {
3682
+ const length = entry.source?.length ?? Number.POSITIVE_INFINITY;
3683
+ const out = [];
3684
+ entry.functions.forEach((fn, i) => {
3685
+ const whole = fn.ranges[0];
3686
+ if (!whole || whole.count <= 0) return;
3687
+ const topLevel = whole.startOffset === 0 && (whole.endOffset >= length || i === 0 && fn.functionName === "");
3688
+ if (topLevel) return;
3689
+ out.push({ start: whole.startOffset, end: whole.endOffset });
3690
+ });
3691
+ return out;
3692
+ }
3693
+ function executedFunctionOffsets(entry) {
3694
+ return executedFunctionRanges(entry).map((range) => range.start);
3695
+ }
3696
+ function scriptRan(entry) {
3697
+ return executedFunctionOffsets(entry).length > 0;
3698
+ }
3699
+ function reachedSources(entry, map) {
3700
+ const ranges = executedFunctionRanges(entry);
3701
+ const reached = /* @__PURE__ */ new Set();
3702
+ if (!ranges.length || entry.source === void 0) return reached;
3703
+ const starts = lineStarts(entry.source);
3704
+ for (const range of ranges) {
3705
+ const start = offsetToPosition(starts, range.start);
3706
+ const source = map.sourceAt(start.line, start.column);
3707
+ if (source === null || reached.has(source)) continue;
3708
+ if (map.mapsBetween(source, start, offsetToPosition(starts, range.end))) reached.add(source);
3709
+ }
3710
+ return reached;
3711
+ }
3712
+ function finalizeCodeReach(files) {
3713
+ const out = /* @__PURE__ */ new Set();
3714
+ for (const raw of files) {
3715
+ const file = raw.replace(/\\/g, "/").replace(/^\.\//, "");
3716
+ if (!file || file.startsWith("../") || file.startsWith("/") || /^[A-Za-z]:\//.test(file)) continue;
3717
+ if (/(^|\/)node_modules\//.test(file)) continue;
3718
+ out.add(file);
3719
+ }
3720
+ return [...out].sort().slice(0, MAX_CODE_REACH_FILES);
3721
+ }
3722
+
3723
+ // src/internal/capture/code-reach.ts
3724
+ var MAX_MAP_BYTES = 20 * 1024 * 1024;
3725
+ var MAP_TIMEOUT_MS = 5e3;
3726
+ var SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|marko|html?)$/i;
3727
+ var REPLAYED_SCRIPT = /^(?:about|rsc):/i;
3728
+ var BUILD_DIRS = /(?:^|\/)(?:dist|build|out|\.output|\.next|\.nuxt|\.svelte-kit|public|static|wwwroot)\//;
3729
+ var MAX_CACHED_SCRIPTS = 500;
3730
+ var LatestByUrl = class {
3731
+ constructor() {
3732
+ this.entries = /* @__PURE__ */ new Map();
3733
+ }
3734
+ get(url, version) {
3735
+ const entry = this.entries.get(url);
3736
+ if (!entry || entry.version !== version) return void 0;
3737
+ this.entries.delete(url);
3738
+ this.entries.set(url, entry);
3739
+ return entry.value;
3740
+ }
3741
+ set(url, version, value) {
3742
+ this.entries.delete(url);
3743
+ this.entries.set(url, { version, value });
3744
+ if (this.entries.size > MAX_CACHED_SCRIPTS) this.entries.delete(this.entries.keys().next().value);
3745
+ }
3746
+ clear() {
3747
+ this.entries.clear();
3748
+ }
3749
+ };
3750
+ var scriptCache = new LatestByUrl();
3751
+ var mapCache = new LatestByUrl();
3752
+ var cachedRepoRoot = null;
3753
+ function repositoryRoot(from) {
3754
+ if (cachedRepoRoot?.from === from) return cachedRepoRoot.root;
3755
+ let root = from;
3756
+ try {
3757
+ root = (0, import_node_child_process.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
3758
+ cwd: from,
3759
+ encoding: "utf-8",
3760
+ stdio: ["ignore", "pipe", "ignore"]
3761
+ }).trim();
3762
+ } catch {
3763
+ }
3764
+ cachedRepoRoot = { from, root: path9.resolve(root) };
3765
+ return cachedRepoRoot.root;
3766
+ }
3767
+ function codeReachRoots(configDir, configured) {
3768
+ const repoRoot = repositoryRoot(configDir);
3769
+ const roots = configured?.length ? configured.map((r) => path9.resolve(configDir, r)) : [configDir];
3770
+ return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
3771
+ }
3772
+ function isFile(file) {
3773
+ try {
3774
+ return fs6.statSync(file).isFile();
3775
+ } catch {
3776
+ return false;
3777
+ }
3778
+ }
3779
+ function resolveSourcePath(source, roots) {
3780
+ const normalized = normalizeSourcePath(source);
3781
+ if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
3782
+ if (path9.isAbsolute(normalized) && isFile(normalized)) return path9.resolve(normalized);
3783
+ const relative7 = normalized.replace(/^\/+/, "");
3784
+ for (const root of roots) {
3785
+ const candidate = path9.resolve(root, relative7);
3786
+ if (isFile(candidate)) return candidate;
3787
+ }
3788
+ return null;
3789
+ }
3790
+ function viteBases(entries) {
3791
+ const bases = /* @__PURE__ */ new Map();
3792
+ for (const entry of entries) {
3793
+ const match = /^(https?:\/\/[^/]+)(\/.*?)@vite\/client$/.exec(entry.url.replace(/[?#].*$/, ""));
3794
+ if (match && match[2] !== "/") bases.set(match[1], match[2]);
3795
+ }
3796
+ return bases;
3797
+ }
3798
+ function fileForUrl(url, roots, bases) {
3799
+ let pathname;
3800
+ try {
3801
+ const parsed = new URL(url);
3802
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" && parsed.protocol !== "file:") return null;
3803
+ pathname = parsed.pathname;
3804
+ const base = bases.get(parsed.origin);
3805
+ if (base && pathname.startsWith(base)) pathname = pathname.slice(base.length - 1);
3806
+ } catch {
3807
+ return null;
3808
+ }
3809
+ if (!SOURCE_EXTENSIONS.test(pathname) || pathname.startsWith("/@vite/") || pathname.includes("/node_modules/")) {
3810
+ return null;
3811
+ }
3812
+ const file = resolveSourcePath(pathname, roots);
3813
+ if (!file || BUILD_DIRS.test(file.replace(/\\/g, "/"))) return null;
3814
+ return file;
3815
+ }
3816
+ function relativeToRepo(file, repoRoot) {
3817
+ return path9.relative(repoRoot, file).split(path9.sep).join("/");
3818
+ }
3819
+ function scriptHash(entry) {
3820
+ return crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
3821
+ }
3822
+ async function loadMap(entry, fetchMap) {
3823
+ const ref = entry.source ? sourceMappingUrl(entry.source) : null;
3824
+ if (!ref) return null;
3825
+ if (ref.startsWith("data:")) {
3826
+ const text = decodeDataUrl(ref);
3827
+ return text ? parseMap(text) : null;
3828
+ }
3829
+ let absolute;
3830
+ try {
3831
+ absolute = new URL(ref, entry.url).href;
3832
+ } catch {
3833
+ return null;
3834
+ }
3835
+ const version = scriptHash(entry);
3836
+ let pending = mapCache.get(absolute, version);
3837
+ if (!pending) {
3838
+ pending = fetchMap(absolute).then((text) => text ? parseMap(text) : null);
3839
+ mapCache.set(absolute, version, pending);
3840
+ }
3841
+ return pending;
3842
+ }
3843
+ function parseMap(text) {
3844
+ try {
3845
+ return decodeSourceMap(JSON.parse(text));
3846
+ } catch {
3847
+ return null;
3848
+ }
3849
+ }
3850
+ async function resolveScript(entry, roots, bases, fetchMap) {
3851
+ const version = scriptHash(entry);
3852
+ const cached = scriptCache.get(entry.url, version);
3853
+ if (cached) return cached;
3854
+ let resolution;
3855
+ const file = fileForUrl(entry.url, roots.roots, bases);
3856
+ if (file) resolution = { kind: "file", file, lookup: await loadMap(entry, fetchMap) };
3857
+ else {
3858
+ const lookup = await loadMap(entry, fetchMap);
3859
+ resolution = lookup ? { kind: "map", lookup, files: [] } : { kind: "none" };
3860
+ }
3861
+ scriptCache.set(entry.url, version, resolution);
3862
+ return resolution;
3863
+ }
3864
+ async function resolveCodeReach(entries, roots, fetchMap) {
3865
+ const files = /* @__PURE__ */ new Set();
3866
+ const bases = viteBases(entries);
3867
+ for (const entry of entries) {
3868
+ if (!entry.url || !entry.functions?.length || REPLAYED_SCRIPT.test(entry.url)) continue;
3869
+ const resolution = await resolveScript(entry, roots, bases, fetchMap);
3870
+ if (resolution.kind === "file") {
3871
+ const ran = resolution.lookup ? reachedSources(entry, resolution.lookup).size > 0 : scriptRan(entry);
3872
+ if (resolution.file && ran) files.add(relativeToRepo(resolution.file, roots.repoRoot));
3873
+ } else if (resolution.kind === "map") {
3874
+ for (const index of reachedSources(entry, resolution.lookup)) {
3875
+ if (resolution.files[index] === void 0) {
3876
+ const source = resolution.lookup.sources[index] ?? "";
3877
+ let absolute = resolveSourcePath(source, roots.roots);
3878
+ if (!absolute && !/^[a-z][\w+.-]*:/i.test(source)) {
3879
+ try {
3880
+ absolute = fileForUrl(new URL(source, entry.url).href, roots.roots, bases);
3881
+ } catch {
3882
+ absolute = null;
3883
+ }
3884
+ }
3885
+ resolution.files[index] = absolute;
3886
+ }
3887
+ const file = resolution.files[index];
3888
+ if (file) files.add(relativeToRepo(file, roots.repoRoot));
3889
+ }
3890
+ }
3891
+ }
3892
+ return finalizeCodeReach(files);
3893
+ }
3894
+ function pageMapFetcher(page) {
3895
+ return async (url) => {
3896
+ try {
3897
+ const response = await internalCall(page, () => page.request.get(url, { timeout: MAP_TIMEOUT_MS }));
3898
+ if (!response.ok()) return null;
3899
+ const length = Number(response.headers()["content-length"] ?? 0);
3900
+ if (length > MAX_MAP_BYTES) return null;
3901
+ const body = await response.body();
3902
+ return body.length > MAX_MAP_BYTES ? null : body.toString("utf-8");
3903
+ } catch {
3904
+ return null;
3905
+ }
3906
+ };
3907
+ }
3908
+ function isChromiumPage(page) {
3909
+ try {
3910
+ return page.context().browser()?.browserType().name() === "chromium";
3911
+ } catch {
3912
+ return false;
3913
+ }
3914
+ }
3915
+ async function startCodeReach(page) {
3916
+ if (!isChromiumPage(page) || !page.coverage) return false;
3917
+ try {
3918
+ await internalCall(
3919
+ page,
3920
+ () => page.coverage.startJSCoverage({ resetOnNavigation: false, reportAnonymousScripts: false })
3921
+ );
3922
+ return true;
3923
+ } catch {
3924
+ return false;
3925
+ }
3926
+ }
3927
+ async function stopCodeReach(page) {
3928
+ try {
3929
+ return await internalCall(page, () => page.coverage.stopJSCoverage());
3930
+ } catch {
3931
+ return null;
3932
+ }
2792
3933
  }
2793
3934
 
2794
3935
  // src/internal/capture/capture-fixtures.ts
@@ -2840,10 +3981,53 @@ function createSink() {
2840
3981
  pageInventories: [],
2841
3982
  probeItem: null,
2842
3983
  probeInterception: null,
3984
+ flake: null,
2843
3985
  pickOffered: false,
2844
- userPick: null
3986
+ userPick: null,
3987
+ locatorPages: new LocatorPageLog(),
3988
+ codeReach: null
2845
3989
  };
2846
3990
  }
3991
+ function maybeStartCodeReach(page) {
3992
+ const sink = currentSink;
3993
+ if (!sink || process.env.PIWI_CAPTURE_CODE_REACH !== "true") return Promise.resolve(false);
3994
+ if (sink.codeReach) return sink.codeReach.page === page ? sink.codeReach.started : Promise.resolve(false);
3995
+ const started = startCodeReach(page);
3996
+ sink.codeReach = { page, started, stopped: false, files: null };
3997
+ return started;
3998
+ }
3999
+ function configuredCodeReachRoots() {
4000
+ const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
4001
+ if (!raw) return null;
4002
+ try {
4003
+ const parsed = JSON.parse(raw);
4004
+ if (Array.isArray(parsed)) return parsed.filter((r) => typeof r === "string" && r !== "");
4005
+ } catch {
4006
+ }
4007
+ return raw.split(path10.delimiter).filter(Boolean);
4008
+ }
4009
+ async function stopSinkCodeReach(sink, closing) {
4010
+ const session = sink.codeReach;
4011
+ if (!session || session.stopped) return;
4012
+ if (closing && closing.page !== session.page && (!closing.context || pageContext(session.page) !== closing.context)) {
4013
+ return;
4014
+ }
4015
+ session.stopped = true;
4016
+ try {
4017
+ if (!await session.started || isPageClosed(session.page)) return;
4018
+ const entries = await stopCodeReach(session.page);
4019
+ if (!entries) return;
4020
+ const info = sink.testInfo;
4021
+ const configFile = info?.config.configFile;
4022
+ const configDir = configFile ? path10.dirname(configFile) : info?.config.rootDir ?? process.cwd();
4023
+ session.files = await resolveCodeReach(
4024
+ entries,
4025
+ codeReachRoots(configDir, configuredCodeReachRoots()),
4026
+ pageMapFetcher(session.page)
4027
+ );
4028
+ } catch {
4029
+ }
4030
+ }
2847
4031
  var currentSink = null;
2848
4032
  var PENDING_PROBES = /* @__PURE__ */ new Set();
2849
4033
  async function drainPendingProbes(capMs) {
@@ -2851,8 +4035,8 @@ async function drainPendingProbes(capMs) {
2851
4035
  let cap;
2852
4036
  await Promise.race([
2853
4037
  Promise.allSettled(PENDING_PROBES),
2854
- new Promise((resolve) => {
2855
- cap = setTimeout(resolve, capMs);
4038
+ new Promise((resolve3) => {
4039
+ cap = setTimeout(resolve3, capMs);
2856
4040
  })
2857
4041
  ]);
2858
4042
  clearTimeout(cap);
@@ -2892,10 +4076,10 @@ async function readWebVitals(page) {
2892
4076
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
2893
4077
  paint[key] = Math.round(entry.startTime);
2894
4078
  }
2895
- const readBuffered = (type, extra) => new Promise((resolve) => {
4079
+ const readBuffered = (type, extra) => new Promise((resolve3) => {
2896
4080
  try {
2897
4081
  const PO = globalThis.PerformanceObserver;
2898
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve(null);
4082
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve3(null);
2899
4083
  const out = [];
2900
4084
  const po = new PO((list) => out.push(...list.getEntries()));
2901
4085
  po.observe({ type, buffered: true, ...extra });
@@ -2905,10 +4089,10 @@ async function readWebVitals(page) {
2905
4089
  po.disconnect();
2906
4090
  } catch {
2907
4091
  }
2908
- resolve(out);
4092
+ resolve3(out);
2909
4093
  }, 0);
2910
4094
  } catch {
2911
- resolve(null);
4095
+ resolve3(null);
2912
4096
  }
2913
4097
  });
2914
4098
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -3021,12 +4205,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
3021
4205
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
3022
4206
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
3023
4207
  function withTimeout(promise, ms) {
3024
- return new Promise((resolve, reject) => {
4208
+ return new Promise((resolve3, reject) => {
3025
4209
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
3026
4210
  promise.then(
3027
4211
  (value) => {
3028
4212
  clearTimeout(timer);
3029
- resolve(value);
4213
+ resolve3(value);
3030
4214
  },
3031
4215
  (error) => {
3032
4216
  clearTimeout(timer);
@@ -3060,7 +4244,7 @@ async function recordPageWindow(sink, page) {
3060
4244
  if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
3061
4245
  if (key && !inventoriedPageKeys.has(key)) {
3062
4246
  inventoriedPageKeys.add(key);
3063
- const content = await readPageInventory(page);
4247
+ const content = await internalCall(page, () => readPageInventory(page));
3064
4248
  if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
3065
4249
  sink.pageInventories.push({
3066
4250
  url: content?.url ?? url,
@@ -3077,6 +4261,9 @@ async function stashPageState(sink, closing) {
3077
4261
  if (!page || isPageClosed(page)) return;
3078
4262
  const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
3079
4263
  if (!belongsToClosing) return;
4264
+ await internalCall(page, () => readPageBeforeClose(sink, page));
4265
+ }
4266
+ async function readPageBeforeClose(sink, page) {
3080
4267
  const vitals = await readWebVitals(page);
3081
4268
  if (vitals) sink.stashedWebVitals = vitals;
3082
4269
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
@@ -3118,7 +4305,10 @@ async function maybeOpenPicker(sink, closing) {
3118
4305
  console.log("[piwi] locator picker: no failing locator could be identified in this failure \u2014 nothing to replace.");
3119
4306
  return;
3120
4307
  }
3121
- const pick = await runLocatorPicker(page, testInfo, failed, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG });
4308
+ const pick = await internalCall(
4309
+ page,
4310
+ () => runLocatorPicker(page, testInfo, failed, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG })
4311
+ );
3122
4312
  if (!pick) return;
3123
4313
  sink.userPick = pick;
3124
4314
  applyPickToSnapshots(sink.capturedLocators, pick);
@@ -3137,12 +4327,8 @@ var CAPTURED_ATTRS_ARG = {
3137
4327
  // special-cased logic in the probe, so add them explicitly).
3138
4328
  roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
3139
4329
  // 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
4330
+ // anchors step and generateAnchoredAlternatives both need them).
4331
+ includeStructural: true
3146
4332
  };
3147
4333
  var PROBE_GLOBAL = "__piwiProbeElement";
3148
4334
  var PROBE_INIT_SCRIPT = `(() => {
@@ -3153,15 +4339,18 @@ var PROBE_INIT_SCRIPT = `(() => {
3153
4339
  var PROBE_UNSEEDED_PAGES = /* @__PURE__ */ new WeakSet();
3154
4340
  function probeElement(page, target) {
3155
4341
  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
- });
4342
+ if (!page || PROBE_UNSEEDED_PAGES.has(page)) return internalCall(page, shipSource);
4343
+ return internalCall(
4344
+ page,
4345
+ () => target.evaluate((el, name) => {
4346
+ const seeded = globalThis[name];
4347
+ return typeof seeded === "function" ? seeded(el) : null;
4348
+ }, PROBE_GLOBAL).then((attrs) => {
4349
+ if (attrs) return attrs;
4350
+ PROBE_UNSEEDED_PAGES.add(page);
4351
+ return shipSource();
4352
+ })
4353
+ );
3165
4354
  }
3166
4355
  async function ariaSnapshotBestEffort(target, timeout) {
3167
4356
  if (typeof target.ariaSnapshot !== "function") return null;
@@ -3212,7 +4401,7 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
3212
4401
  const role = resolveAriaRole({ ...attrs, accessibleName: null });
3213
4402
  const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
3214
4403
  const exactName = exactAccessibleName(attrs, role);
3215
- const aria = exactName === null && (role || isFormField) ? await ariaSnapshotBestEffort(target, 500) : null;
4404
+ const aria = exactName === null && (role || isFormField) ? await internalCall(page, () => ariaSnapshotBestEffort(target, 500)) : null;
3216
4405
  const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
3217
4406
  sink.capturedLocators[seq] = {
3218
4407
  location: callerLocation,
@@ -3240,15 +4429,23 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
3240
4429
  })();
3241
4430
  sink.capturePromises.push(resolveAttrs);
3242
4431
  }
4432
+ function callMethod(target, prop, args) {
4433
+ return target[prop](...args);
4434
+ }
4435
+ function recordLocatorPage(sink, page, locator, location, arrival) {
4436
+ try {
4437
+ sink.locatorPages.record({ location, locator: String(locator), url: page.url(), arrival });
4438
+ } catch {
4439
+ }
4440
+ }
3243
4441
  function wrapLocator(page, locator, originMethod, originArgs) {
3244
4442
  return new Proxy(locator, {
3245
4443
  get(target, prop) {
3246
4444
  const original = Reflect.get(target, prop);
3247
4445
  if (typeof original !== "function") return original;
3248
- const fn = original;
3249
4446
  if (CHAIN_METHOD_SET.has(prop)) {
3250
4447
  return (...args) => {
3251
- const next = fn.apply(target, args);
4448
+ const next = callMethod(target, prop, args);
3252
4449
  if (LOCATOR_CREATING_CHAINS.has(prop)) {
3253
4450
  return wrapLocator(page, next, String(prop), args);
3254
4451
  }
@@ -3260,11 +4457,12 @@ function wrapLocator(page, locator, originMethod, originArgs) {
3260
4457
  const sink = currentSink;
3261
4458
  const expression = typeof callArgs[0] === "string" ? callArgs[0] : "";
3262
4459
  const isNot = Boolean(callArgs[1]?.isNot);
4460
+ const callerLocation = sink ? captureCallerLocation() : null;
4461
+ if (sink) recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, EXPECT_METHOD, true));
3263
4462
  if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
3264
- return fn.apply(target, callArgs);
4463
+ return callMethod(target, prop, callArgs);
3265
4464
  }
3266
4465
  sink.lastActivePage = page;
3267
- const callerLocation = captureCallerLocation();
3268
4466
  const used = {
3269
4467
  method: originMethod,
3270
4468
  args: originArgs,
@@ -3276,7 +4474,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
3276
4474
  seq = sink.capturedLocators.length;
3277
4475
  sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
3278
4476
  }
3279
- const result = await fn.apply(target, callArgs);
4477
+ const result = await callMethod(target, prop, callArgs);
3280
4478
  const matches = result?.matches;
3281
4479
  if (matches === true && !alreadyProbed) {
3282
4480
  if (callerLocation) sink.probedLocations.add(callerLocation);
@@ -3290,9 +4488,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
3290
4488
  if (!ACTION_METHOD_SET.has(prop)) return original;
3291
4489
  return async (...callArgs) => {
3292
4490
  const sink = currentSink;
3293
- if (!sink) return fn.apply(target, callArgs);
4491
+ if (!sink) return callMethod(target, prop, callArgs);
3294
4492
  sink.lastActivePage = page;
3295
4493
  const callerLocation = captureCallerLocation();
4494
+ recordLocatorPage(sink, page, target, callerLocation, noteLocatorCall(page, String(prop)));
3296
4495
  const used = {
3297
4496
  method: originMethod,
3298
4497
  args: originArgs,
@@ -3311,7 +4510,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
3311
4510
  }
3312
4511
  let result;
3313
4512
  try {
3314
- result = await fn.apply(target, callArgs);
4513
+ result = await callMethod(target, prop, callArgs);
3315
4514
  } catch (error) {
3316
4515
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
3317
4516
  throw error;
@@ -3330,18 +4529,19 @@ function wrapFrameLocator(page, frameLocator) {
3330
4529
  get(target, prop) {
3331
4530
  const original = Reflect.get(target, prop);
3332
4531
  if (typeof original !== "function") return original;
3333
- const fn = original;
3334
4532
  if (!LOCATOR_METHOD_SET.has(prop)) return original;
3335
4533
  return (...args) => {
3336
4534
  if (currentSink) currentSink.lastActivePage = page;
3337
- return wrapLocator(page, fn.apply(target, args), String(prop), args);
4535
+ return wrapLocator(page, callMethod(target, prop, args), String(prop), args);
3338
4536
  };
3339
4537
  }
3340
4538
  });
3341
4539
  }
4540
+ var CANCELLED_REQUEST = /^net::ERR_ABORTED$|^NS_BINDING_ABORTED$|^NS_ERROR_ABORT$|cancell?ed/i;
3342
4541
  function instrumentPage(page) {
3343
4542
  if (!page || INSTRUMENTED_PAGES.has(page)) return;
3344
4543
  INSTRUMENTED_PAGES.add(page);
4544
+ void maybeStartCodeReach(page);
3345
4545
  const probeSink = currentSink;
3346
4546
  if (probeSink?.probeItem && !probeSink.probeInterception) {
3347
4547
  void installProbeInterception(page, probeSink.probeItem).then((interception) => {
@@ -3349,6 +4549,18 @@ function instrumentPage(page) {
3349
4549
  }).catch(() => {
3350
4550
  });
3351
4551
  }
4552
+ const flake = currentSink?.flake;
4553
+ if (flake?.role === "target" && !flake.conditions) {
4554
+ flake.browserName = pageBrowserName(page) ?? flake.browserName;
4555
+ const plan = flake.plan;
4556
+ flake.conditions = installFlakeConditions(page, plan).catch((error) => {
4557
+ const note = `conditions failed to install: ${error instanceof Error ? error.message : String(error)}`;
4558
+ const reports = unappliedReports(plan).map(
4559
+ (r) => r.outcome === "by-command" ? r : { ...r, outcome: "skipped", note }
4560
+ );
4561
+ return { reports: () => reports };
4562
+ });
4563
+ }
3352
4564
  const ctx = pageContext(page);
3353
4565
  if (ctx) instrumentContext(ctx);
3354
4566
  if (typeof page.close === "function") {
@@ -3359,6 +4571,7 @@ function instrumentPage(page) {
3359
4571
  if (sink) {
3360
4572
  await stashPageState(sink, { page });
3361
4573
  await maybeOpenPicker(sink, { page });
4574
+ await stopSinkCodeReach(sink, { page });
3362
4575
  }
3363
4576
  return originalClose(...args);
3364
4577
  };
@@ -3381,7 +4594,13 @@ function instrumentPage(page) {
3381
4594
  };
3382
4595
  }
3383
4596
  if (typeof page.on === "function") {
3384
- page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
4597
+ page.on("framenavigated", (frame) => {
4598
+ PROBE_UNSEEDED_PAGES.delete(page);
4599
+ try {
4600
+ if (frame === page.mainFrame()) noteNavigation(page);
4601
+ } catch {
4602
+ }
4603
+ });
3385
4604
  }
3386
4605
  }
3387
4606
  if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
@@ -3424,7 +4643,7 @@ function instrumentPage(page) {
3424
4643
  } catch {
3425
4644
  }
3426
4645
  }
3427
- page.on("requestfinished", (request) => {
4646
+ const recordRequest = (request, failed) => {
3428
4647
  const sink = currentSink;
3429
4648
  if (!sink) return;
3430
4649
  sink.lastActivePage = page;
@@ -3432,18 +4651,21 @@ function instrumentPage(page) {
3432
4651
  try {
3433
4652
  const url = request.url();
3434
4653
  if (url.startsWith("data:") || url.startsWith("blob:")) return;
3435
- const timing = request.timing();
3436
- const response = await request.response();
3437
4654
  const resourceType = request.resourceType();
3438
- if (!["fetch", "xhr", "document", "other"].includes(resourceType)) return;
4655
+ if (!TRACKED_REQUEST_TYPES.includes(resourceType)) return;
4656
+ const failure = failed ? request.failure()?.errorText ?? "failed" : null;
4657
+ if (failure && CANCELLED_REQUEST.test(failure)) return;
4658
+ const timing = request.timing();
4659
+ const response = failed ? null : await request.response();
3439
4660
  const entry = {
3440
4661
  method: request.method(),
3441
4662
  url,
3442
4663
  status: response ? response.status() : 0,
3443
- duration: timing.responseEnd > 0 ? Math.round(timing.responseEnd - timing.requestStart) : 0,
4664
+ duration: requestDuration(timing, failed),
3444
4665
  startTime: timing.startTime,
3445
4666
  resourceType
3446
4667
  };
4668
+ if (failure) entry.failure = failure;
3447
4669
  if (response) {
3448
4670
  const headers = response.headers();
3449
4671
  const contentType = headers["content-type"];
@@ -3468,7 +4690,12 @@ function instrumentPage(page) {
3468
4690
  }
3469
4691
  })();
3470
4692
  sink.pendingHandlers.push(p);
3471
- });
4693
+ };
4694
+ page.on("requestfinished", (request) => recordRequest(request, false));
4695
+ page.on("requestfailed", (request) => recordRequest(request, true));
4696
+ }
4697
+ async function awaitFlakeConditions() {
4698
+ await currentSink?.flake?.conditions;
3472
4699
  }
3473
4700
  function instrumentContext(context) {
3474
4701
  if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
@@ -3481,6 +4708,8 @@ function instrumentContext(context) {
3481
4708
  context.newPage = async (...args) => {
3482
4709
  const page = await originalNewPage(...args);
3483
4710
  instrumentPage(page);
4711
+ await awaitFlakeConditions();
4712
+ await maybeStartCodeReach(page);
3484
4713
  return page;
3485
4714
  };
3486
4715
  if (typeof context.close === "function") {
@@ -3491,6 +4720,7 @@ function instrumentContext(context) {
3491
4720
  if (sink) {
3492
4721
  await stashPageState(sink, { context });
3493
4722
  await maybeOpenPicker(sink, { context });
4723
+ await stopSinkCodeReach(sink, { context });
3494
4724
  }
3495
4725
  return originalClose(...args);
3496
4726
  };
@@ -3504,6 +4734,8 @@ function patchBrowser(browser) {
3504
4734
  browser.newPage = async (...args) => {
3505
4735
  const page = await originalNewPage(...args);
3506
4736
  instrumentPage(page);
4737
+ await awaitFlakeConditions();
4738
+ await maybeStartCodeReach(page);
3507
4739
  return page;
3508
4740
  };
3509
4741
  const originalNewContext = browser.newContext.bind(browser);
@@ -3520,13 +4752,25 @@ function patchBrowser(browser) {
3520
4752
  };
3521
4753
  }
3522
4754
  }
4755
+ function readRootAria(page) {
4756
+ return internalCall(page, () => ariaSnapshotBestEffort(page.locator(":root")));
4757
+ }
4758
+ function readRootAriaJson(page) {
4759
+ return internalCall(page, () => ariaSnapshotJSONBestEffort(page.locator(":root")));
4760
+ }
4761
+ var TRACKED_REQUEST_TYPES = ["fetch", "xhr", "document", "other"];
4762
+ function requestDuration(timing, failed) {
4763
+ if (timing.responseEnd > 0) return Math.round(timing.responseEnd - timing.requestStart);
4764
+ if (failed && timing.startTime > 0) return Math.max(0, Math.round(Date.now() - timing.startTime));
4765
+ return 0;
4766
+ }
3523
4767
  async function flushSink(sink, testInfo) {
3524
4768
  await Promise.allSettled(sink.pendingHandlers);
3525
4769
  let drainDeadline;
3526
4770
  await Promise.race([
3527
4771
  Promise.allSettled(sink.capturePromises),
3528
- new Promise((resolve) => {
3529
- drainDeadline = setTimeout(resolve, 2e3);
4772
+ new Promise((resolve3) => {
4773
+ drainDeadline = setTimeout(resolve3, 2e3);
3530
4774
  })
3531
4775
  ]);
3532
4776
  clearTimeout(drainDeadline);
@@ -3542,15 +4786,28 @@ async function flushSink(sink, testInfo) {
3542
4786
  body: Buffer.from(JSON.stringify(dedupeSnapshotsByLocation(sink.capturedLocators)))
3543
4787
  });
3544
4788
  }
4789
+ await stopSinkCodeReach(sink);
4790
+ if (sink.codeReach?.files?.length) {
4791
+ await testInfo.attach(ATTACHMENT_NAMES.codeReach, {
4792
+ contentType: "application/json",
4793
+ body: Buffer.from(JSON.stringify(sink.codeReach.files))
4794
+ });
4795
+ }
4796
+ if (sink.locatorPages.size > 0) {
4797
+ await testInfo.attach(ATTACHMENT_NAMES.locatorPages, {
4798
+ contentType: "application/json",
4799
+ body: Buffer.from(JSON.stringify(sink.locatorPages.list()))
4800
+ });
4801
+ }
3545
4802
  if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
3546
4803
  try {
3547
- const snapshot = (pageReadable ? await ariaSnapshotBestEffort(page.locator(":root")) : null) ?? sink.stashedAria;
4804
+ const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
3548
4805
  if (snapshot) {
3549
4806
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
3550
4807
  contentType: "text/plain",
3551
4808
  body: snapshot
3552
4809
  });
3553
- const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
4810
+ const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
3554
4811
  if (snapshotJson) {
3555
4812
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
3556
4813
  contentType: "application/json",
@@ -3575,13 +4832,13 @@ async function flushSink(sink, testInfo) {
3575
4832
  }
3576
4833
  if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
3577
4834
  try {
3578
- const snapshot = (pageReadable ? await ariaSnapshotBestEffort(page.locator(":root")) : null) ?? sink.stashedAria;
4835
+ const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
3579
4836
  if (snapshot) {
3580
4837
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
3581
4838
  contentType: "text/plain",
3582
4839
  body: snapshot
3583
4840
  });
3584
- const snapshotJson = (pageReadable ? await ariaSnapshotJSONBestEffort(page.locator(":root")) : null) ?? sink.stashedAriaJson;
4841
+ const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
3585
4842
  if (snapshotJson) {
3586
4843
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
3587
4844
  contentType: "application/json",
@@ -3627,7 +4884,7 @@ async function flushSink(sink, testInfo) {
3627
4884
  body: Buffer.from(JSON.stringify(sink.networkRequests))
3628
4885
  });
3629
4886
  }
3630
- const webVitals = (pageReadable ? await readWebVitals(page) : null) ?? sink.stashedWebVitals;
4887
+ const webVitals = (pageReadable ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
3631
4888
  if (webVitals) {
3632
4889
  await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
3633
4890
  contentType: "application/json",
@@ -3635,7 +4892,7 @@ async function flushSink(sink, testInfo) {
3635
4892
  });
3636
4893
  }
3637
4894
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
3638
- const pageState = (pageReadable ? await readPageState(page) : null) ?? sink.stashedPageState;
4895
+ const pageState = (pageReadable ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
3639
4896
  if (pageState) {
3640
4897
  await testInfo.attach(ATTACHMENT_NAMES.pageState, {
3641
4898
  contentType: "application/json",
@@ -3669,6 +4926,8 @@ var piwiFixtures = {
3669
4926
  // never double-wraps a page the browser patch already instrumented.
3670
4927
  page: async ({ page }, use) => {
3671
4928
  instrumentPage(page);
4929
+ await awaitFlakeConditions();
4930
+ await maybeStartCodeReach(page);
3672
4931
  await use(page);
3673
4932
  },
3674
4933
  // Auto, test-scoped: open a capture sink for the running test and flush it
@@ -3676,8 +4935,24 @@ var piwiFixtures = {
3676
4935
  // requested, so suites that never destructure `page` are still captured.
3677
4936
  piwiCapture: [
3678
4937
  async ({}, use, testInfo) => {
4938
+ boxCaptureFrames();
4939
+ const conflict = labModeConflict();
4940
+ if (conflict) throw new Error(conflict);
3679
4941
  const sink = createSink();
3680
4942
  sink.testInfo = testInfo;
4943
+ if (isFlakeMode()) {
4944
+ const plan = loadFlakePlan();
4945
+ const role = flakeRoleForTest(plan, {
4946
+ file: testInfo.file,
4947
+ title: testInfo.title,
4948
+ titlePath: testInfo.titlePath,
4949
+ project: testInfo.project.name
4950
+ });
4951
+ if (role) {
4952
+ const browserName = testInfo.project.use?.browserName ?? null;
4953
+ sink.flake = { plan, role, startedAt: Date.now(), browserName, conditions: null };
4954
+ }
4955
+ }
3681
4956
  if (isProbeMode())
3682
4957
  sink.probeItem = probeItemForTest({
3683
4958
  title: testInfo.title,
@@ -3708,23 +4983,57 @@ var piwiFixtures = {
3708
4983
  handled
3709
4984
  });
3710
4985
  }
4986
+ if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
3711
4987
  await flushSink(sink, testInfo);
3712
4988
  }
3713
4989
  },
3714
4990
  { auto: true }
3715
4991
  ]
3716
4992
  };
4993
+ async function recordFlakeAttempt(flake, testInfo) {
4994
+ const installed = flake.conditions ? await flake.conditions : null;
4995
+ recordFlakeResult(
4996
+ flakeResultLine(flake.plan, {
4997
+ role: flake.role,
4998
+ file: testInfo.file,
4999
+ title: testInfo.title,
5000
+ project: testInfo.project.name || null,
5001
+ browserName: flake.browserName,
5002
+ status: testInfo.status ?? "passed",
5003
+ errorText: joinErrorMessages(testInfo.errors),
5004
+ startedAt: flake.startedAt,
5005
+ duration: Date.now() - flake.startedAt,
5006
+ workerIndex: testInfo.workerIndex,
5007
+ parallelIndex: testInfo.parallelIndex,
5008
+ repeatEachIndex: testInfo.repeatEachIndex,
5009
+ retry: testInfo.retry,
5010
+ conditions: installed ? installed.reports() : unappliedReports(flake.plan)
5011
+ })
5012
+ );
5013
+ }
3717
5014
  function extendPiwiFixtures(test) {
3718
5015
  return test.extend(piwiFixtures);
3719
5016
  }
3720
5017
  // Annotate the CommonJS export names for ESM import in node:
3721
5018
  0 && (module.exports = {
3722
5019
  CAPTURED_ATTRS_ARG,
5020
+ LocatorPageLog,
3723
5021
  ariaSnapshotBestEffort,
3724
5022
  ariaSnapshotJSONBestEffort,
5023
+ boxCaptureFrames,
3725
5024
  buildPageState,
5025
+ callMethod,
5026
+ codeReachRoots,
3726
5027
  computeCoreVitals,
5028
+ configuredCodeReachRoots,
3727
5029
  extendPiwiFixtures,
5030
+ internalCall,
5031
+ noteLocatorCall,
5032
+ noteNavigation,
5033
+ pageMapFetcher,
3728
5034
  piwiFixtures,
3729
- probeElementAttrs
5035
+ probeElementAttrs,
5036
+ resolveCodeReach,
5037
+ startCodeReach,
5038
+ stopCodeReach
3730
5039
  });