@piwitests/reporter 0.40.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.
@@ -37,15 +37,22 @@ __export(capture_fixtures_exports, {
37
37
  boxCaptureFrames: () => boxCaptureFrames,
38
38
  buildPageState: () => buildPageState,
39
39
  callMethod: () => callMethod,
40
+ codeReachRoots: () => codeReachRoots,
40
41
  computeCoreVitals: () => computeCoreVitals,
42
+ configuredCodeReachRoots: () => configuredCodeReachRoots,
41
43
  extendPiwiFixtures: () => extendPiwiFixtures,
42
44
  internalCall: () => internalCall,
43
45
  noteLocatorCall: () => noteLocatorCall,
44
46
  noteNavigation: () => noteNavigation,
47
+ pageMapFetcher: () => pageMapFetcher,
45
48
  piwiFixtures: () => piwiFixtures,
46
- probeElementAttrs: () => probeElementAttrs
49
+ probeElementAttrs: () => probeElementAttrs,
50
+ resolveCodeReach: () => resolveCodeReach,
51
+ startCodeReach: () => startCodeReach,
52
+ stopCodeReach: () => stopCodeReach
47
53
  });
48
54
  module.exports = __toCommonJS(capture_fixtures_exports);
55
+ var path10 = __toESM(require("path"));
49
56
  var import_node_zlib2 = require("zlib");
50
57
 
51
58
  // ../picker-dom/src/probe.ts
@@ -521,6 +528,11 @@ function installPickerOverlay(arg) {
521
528
  placeLabel(r);
522
529
  foot.textContent = "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip";
523
530
  };
531
+ const redescribe = () => {
532
+ labeled = null;
533
+ if (current()) refresh();
534
+ };
535
+ g.__piwiRedescribe = redescribe;
524
536
  const stop = (e) => {
525
537
  e.preventDefault();
526
538
  e.stopImmediatePropagation();
@@ -575,6 +587,7 @@ function installPickerOverlay(arg) {
575
587
  highlight.remove();
576
588
  label.remove();
577
589
  banner.remove();
590
+ if (g.__piwiRedescribe === redescribe) delete g.__piwiRedescribe;
578
591
  };
579
592
  const reportPicked = (el) => {
580
593
  removePickingListeners();
@@ -1121,8 +1134,9 @@ function isAutoGenerated(value) {
1121
1134
  }
1122
1135
  if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
1123
1136
  if (value.startsWith("ng-")) return true;
1124
- 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;
1125
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;
1126
1140
  return false;
1127
1141
  }
1128
1142
  var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
@@ -1778,6 +1792,7 @@ var ATTACHMENT_NAMES = {
1778
1792
  network: "piwi-network",
1779
1793
  pageInventory: "piwi-page-inventory",
1780
1794
  locatorPages: "piwi-locator-pages",
1795
+ codeReach: "piwi-code-reach",
1781
1796
  webVitals: "piwi-web-vitals",
1782
1797
  locatorSuggestion: "piwi-locator-suggestion",
1783
1798
  pageState: "piwi-page-state",
@@ -1938,6 +1953,11 @@ function pageKey(url) {
1938
1953
  const q = normalized.indexOf("?");
1939
1954
  return q < 0 ? normalized : normalized.slice(0, q);
1940
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
+ }
1941
1961
 
1942
1962
  // src/internal/capture/locator-pages.ts
1943
1963
  var MAX_LOCATOR_PAGE_ENTRIES = 1e3;
@@ -2015,6 +2035,8 @@ var DEFAULTS = {
2015
2035
  // Off by default: the page inventory reads control and link names from every
2016
2036
  // visited page, so it stays opt-in until a project turns it on.
2017
2037
  capturePageInventory: false,
2038
+ // Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
2039
+ captureCodeReach: false,
2018
2040
  captureServerTraces: true,
2019
2041
  sampleAriaOnPass: true,
2020
2042
  uploadManifest: true,
@@ -2051,6 +2073,8 @@ var PIWI_ENV_KEYS = {
2051
2073
  captureLocators: "PIWI_CAPTURE_LOCATORS",
2052
2074
  capturePageState: "PIWI_CAPTURE_PAGE_STATE",
2053
2075
  capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
2076
+ captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
2077
+ codeReachRoots: "PIWI_CODE_REACH_ROOTS",
2054
2078
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
2055
2079
  sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
2056
2080
  uploadManifest: "PIWI_UPLOAD_MANIFEST",
@@ -2073,6 +2097,10 @@ var PIWI_PROBE_ENV = {
2073
2097
  results: "PIWI_PROBE_RESULTS",
2074
2098
  secret: "PIWI_PROBE_SECRET"
2075
2099
  };
2100
+ var PIWI_FLAKE_ENV = {
2101
+ plan: "PIWI_FLAKE_PLAN",
2102
+ results: "PIWI_FLAKE_RESULTS"
2103
+ };
2076
2104
  var ENV_FALLBACK_SPECS = [
2077
2105
  { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
2078
2106
  { option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
@@ -2094,6 +2122,7 @@ var ENV_FALLBACK_SPECS = [
2094
2122
  { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
2095
2123
  { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
2096
2124
  { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
2125
+ { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
2097
2126
  { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
2098
2127
  { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
2099
2128
  { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
@@ -2104,14 +2133,14 @@ var ENV_FALLBACK_SPECS = [
2104
2133
  ];
2105
2134
 
2106
2135
  // src/internal/probe/plan.ts
2107
- function parseRouteKey(routeKey) {
2108
- const space = routeKey.indexOf(" ");
2109
- if (space < 0) return { method: "", pattern: routeKey };
2110
- 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) };
2111
2140
  }
2112
2141
  function patternToRegex(pattern) {
2113
- const path8 = pattern.split("?")[0] ?? pattern;
2114
- const escaped = path8.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, "[^/]+");
2115
2144
  return new RegExp(`^${escaped}$`);
2116
2145
  }
2117
2146
  function suitePathEqual(a, b) {
@@ -2131,16 +2160,16 @@ function matchProbeItem(plan, test) {
2131
2160
  }
2132
2161
  return null;
2133
2162
  }
2134
- function requestMatchesRoute(method, url, routeKey) {
2135
- const { method: rm, pattern } = parseRouteKey(routeKey);
2163
+ function requestMatchesRoute(method, url, routeKey2) {
2164
+ const { method: rm, pattern } = parseRouteKey(routeKey2);
2136
2165
  if (rm && rm !== method.toUpperCase()) return false;
2137
- let path8;
2166
+ let path11;
2138
2167
  try {
2139
- path8 = new URL(url).pathname;
2168
+ path11 = new URL(url).pathname;
2140
2169
  } catch {
2141
- path8 = url.split("?")[0] ?? url;
2170
+ path11 = url.split("?")[0] ?? url;
2142
2171
  }
2143
- return patternToRegex(pattern).test(path8);
2172
+ return patternToRegex(pattern).test(path11);
2144
2173
  }
2145
2174
  function createProbeState() {
2146
2175
  return { navigated: false, matches: 0 };
@@ -2148,10 +2177,10 @@ function createProbeState() {
2148
2177
  function markNavigated(state) {
2149
2178
  state.navigated = true;
2150
2179
  }
2151
- function shouldMutate(state, item) {
2180
+ function shouldAct(state, match) {
2152
2181
  if (!state.navigated) return false;
2153
2182
  state.matches += 1;
2154
- return state.matches === Math.max(1, item.nth ?? 1);
2183
+ return match === "all" || state.matches === Math.max(1, match);
2155
2184
  }
2156
2185
 
2157
2186
  // src/internal/probe/mode.ts
@@ -2162,10 +2191,10 @@ function isProbeMode() {
2162
2191
  function loadPlan() {
2163
2192
  if (planCache !== void 0) return planCache;
2164
2193
  planCache = null;
2165
- const path8 = process.env[PIWI_PROBE_ENV.plan];
2166
- if (path8) {
2194
+ const path11 = process.env[PIWI_PROBE_ENV.plan];
2195
+ if (path11) {
2167
2196
  try {
2168
- planCache = JSON.parse(fs2.readFileSync(path8, "utf8"));
2197
+ planCache = JSON.parse(fs2.readFileSync(path11, "utf8"));
2169
2198
  } catch {
2170
2199
  planCache = null;
2171
2200
  }
@@ -2187,10 +2216,10 @@ function outcomeFromStatus(status, applied) {
2187
2216
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2188
2217
  }
2189
2218
  function recordProbeOutcome(line) {
2190
- const path8 = process.env[PIWI_PROBE_ENV.results];
2191
- if (!path8) return;
2219
+ const path11 = process.env[PIWI_PROBE_ENV.results];
2220
+ if (!path11) return;
2192
2221
  try {
2193
- fs2.appendFileSync(path8, `${JSON.stringify(line)}
2222
+ fs2.appendFileSync(path11, `${JSON.stringify(line)}
2194
2223
  `);
2195
2224
  } catch {
2196
2225
  }
@@ -2217,6 +2246,9 @@ function isFaultAllowed(level, fault) {
2217
2246
  return list.includes(fault);
2218
2247
  }
2219
2248
  var SLOW_FAULT_DELAY_MS = 5e3;
2249
+ function faultRouteAction(fault) {
2250
+ return fault === "slow" ? { kind: "delay", ms: SLOW_FAULT_DELAY_MS } : null;
2251
+ }
2220
2252
  function dropOneField(body) {
2221
2253
  let parsed;
2222
2254
  try {
@@ -2317,11 +2349,46 @@ function originOf(url) {
2317
2349
  return null;
2318
2350
  }
2319
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
+ }
2320
2388
  async function installProbeInterception(page, item) {
2321
2389
  if (!isFaultAllowed(item.level ?? "client", item.fault)) {
2322
2390
  return { applied: () => false, appliedFault: () => null, serverError: () => false };
2323
2391
  }
2324
- const state = createProbeState();
2325
2392
  let applied = false;
2326
2393
  let appliedFault = null;
2327
2394
  let serverError = false;
@@ -2329,17 +2396,12 @@ async function installProbeInterception(page, item) {
2329
2396
  const probeSecret = process.env[PIWI_PROBE_ENV.secret];
2330
2397
  page.on("framenavigated", (frame) => {
2331
2398
  if (frame === page.mainFrame()) {
2332
- markNavigated(state);
2333
2399
  const origin = originOf(page.url());
2334
2400
  if (origin) appOrigin = origin;
2335
2401
  }
2336
2402
  });
2337
- await page.route("**/*", async (route) => {
2403
+ const handle = async (route, startedAt) => {
2338
2404
  const request = route.request();
2339
- if (applied || !requestMatchesRoute(request.method(), request.url(), item.routeKey) || !shouldMutate(state, item)) {
2340
- await route.fallback();
2341
- return;
2342
- }
2343
2405
  if (item.level === "server") {
2344
2406
  if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
2345
2407
  await route.fallback();
@@ -2363,6 +2425,13 @@ async function installProbeInterception(page, item) {
2363
2425
  return;
2364
2426
  }
2365
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
+ }
2366
2435
  const response = await route.fetch();
2367
2436
  const body = await response.text();
2368
2437
  const out = computeFault(item.fault, {
@@ -2372,53 +2441,26 @@ async function installProbeInterception(page, item) {
2372
2441
  });
2373
2442
  applied = out.changed;
2374
2443
  if (applied) appliedFault = item.fault;
2375
- if (out.delayMs > 0) await new Promise((resolve2) => setTimeout(resolve2, out.delayMs));
2376
2444
  const headers = fulfillHeaders(response.headers(), out.contentType);
2377
2445
  await route.fulfill({ status: out.status, body: out.body, headers });
2378
2446
  } catch {
2379
2447
  applied = false;
2380
2448
  await route.fallback();
2381
2449
  }
2382
- });
2383
- return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
2384
- }
2385
-
2386
- // src/internal/capture/inspect-on-failure.ts
2387
- function isCi(ci) {
2388
- return ci !== void 0 && ci !== "" && ci !== "false";
2389
- }
2390
- function shouldInspectOnFailure(gate) {
2391
- if (gate.enabled !== "true") return false;
2392
- if (isCi(gate.ci)) return false;
2393
- if (gate.headless !== false) return false;
2394
- if (gate.status !== "failed" && gate.status !== "timedOut") return false;
2395
- if (gate.status === gate.expectedStatus) return false;
2396
- return gate.retry >= gate.retries;
2397
- }
2398
- function environmentalSkipReason(gate) {
2399
- if (gate.enabled !== "true") return null;
2400
- if (gate.status !== "failed" && gate.status !== "timedOut") return null;
2401
- if (gate.status === gate.expectedStatus) return null;
2402
- if (isCi(gate.ci)) return "running under CI \u2014 this is a headed, local-only feature";
2403
- if (gate.headless !== false) {
2404
- return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
2405
- }
2406
- return null;
2407
- }
2408
- function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT_ON_FAIL) {
2409
- const use = testInfo.project?.use ?? {};
2410
- return {
2411
- enabled,
2412
- ci: process.env.CI,
2413
- status: testInfo.status,
2414
- expectedStatus: testInfo.expectedStatus,
2415
- headless: use.headless,
2416
- retry: testInfo.retry,
2417
- retries: testInfo.project?.retries ?? 0
2418
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 };
2419
2460
  }
2420
2461
 
2421
- // src/internal/capture/pick-on-failure.ts
2462
+ // src/internal/flake/mode.ts
2463
+ var fs3 = __toESM(require("fs"));
2422
2464
  var path4 = __toESM(require("path"));
2423
2465
 
2424
2466
  // ../core/src/locator-chain.ts
@@ -2703,8 +2745,474 @@ function locatorCallValues(call, opts = {}) {
2703
2745
  return call.args.map(value).filter((v) => v !== void 0);
2704
2746
  }
2705
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
+
2706
3213
  // src/internal/capture/pick-on-failure.ts
2707
- var ANSI_RE = /\[[0-9;]*m/g;
3214
+ var path5 = __toESM(require("path"));
3215
+ var ANSI_RE2 = /\[[0-9;]*m/g;
2708
3216
  function parseLeafLocatorExpression(rawExpr) {
2709
3217
  const leaf = parseLeafLocatorCall(rawExpr);
2710
3218
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -2714,13 +3222,13 @@ function deriveFailedLocator(testInfo) {
2714
3222
  const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
2715
3223
  for (const err of errors) {
2716
3224
  const text = `${err.message ?? ""}
2717
- ${err.stack ?? ""}`.replace(ANSI_RE, "");
3225
+ ${err.stack ?? ""}`.replace(ANSI_RE2, "");
2718
3226
  const line = /^\s*Locator:\s*(.+)$/m.exec(text);
2719
3227
  if (!line) continue;
2720
3228
  const parsed = parseLeafLocatorExpression(line[1].trim());
2721
3229
  if (!parsed) continue;
2722
3230
  const loc = err.location;
2723
- 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;
2724
3232
  return { method: parsed.method, args: parsed.args, location };
2725
3233
  }
2726
3234
  return null;
@@ -2869,9 +3377,9 @@ function applyPickToSnapshots(snapshots, pick) {
2869
3377
  }
2870
3378
 
2871
3379
  // src/internal/support/aria-sampling.ts
2872
- var path5 = __toESM(require("path"));
3380
+ var path6 = __toESM(require("path"));
2873
3381
  var os3 = __toESM(require("os"));
2874
- var fs3 = __toESM(require("fs"));
3382
+ var fs4 = __toESM(require("fs"));
2875
3383
 
2876
3384
  // src/internal/support/instance-id.ts
2877
3385
  var crypto = __toESM(require("crypto"));
@@ -2885,14 +3393,14 @@ function ariaSampleIdentity(filePath, title) {
2885
3393
  return `${filePath}\0${title}`;
2886
3394
  }
2887
3395
  function getAriaSampleFilePath(projectName) {
2888
- 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`);
2889
3397
  }
2890
3398
  var cachedSets = /* @__PURE__ */ new Map();
2891
3399
  function loadAriaSampleSet(projectName) {
2892
3400
  if (cachedSets.has(projectName)) return cachedSets.get(projectName);
2893
3401
  let set = null;
2894
3402
  try {
2895
- const raw = fs3.readFileSync(getAriaSampleFilePath(projectName), "utf8");
3403
+ const raw = fs4.readFileSync(getAriaSampleFilePath(projectName), "utf8");
2896
3404
  const parsed = JSON.parse(raw);
2897
3405
  if (parsed.projectName === projectName && Array.isArray(parsed.identities)) {
2898
3406
  set = new Set(parsed.identities.filter((x) => typeof x === "string"));
@@ -2903,33 +3411,33 @@ function loadAriaSampleSet(projectName) {
2903
3411
  cachedSets.set(projectName, set);
2904
3412
  return set;
2905
3413
  }
2906
- function relativeTestFile(file) {
2907
- 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("/");
2908
3416
  }
2909
3417
  function isDueForAriaSample(testInfo) {
2910
3418
  const projectName = process.env.PIWI_PROJECT_NAME;
2911
3419
  if (!projectName) return false;
2912
3420
  const set = loadAriaSampleSet(projectName);
2913
3421
  if (!set || set.size === 0) return false;
2914
- return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
3422
+ return set.has(ariaSampleIdentity(relativeTestFile2(testInfo.file), testInfo.title));
2915
3423
  }
2916
3424
 
2917
3425
  // src/internal/capture/quiet-capture.ts
2918
3426
  var import_node_module = require("module");
2919
- var path7 = __toESM(require("path"));
3427
+ var path8 = __toESM(require("path"));
2920
3428
 
2921
3429
  // src/internal/support/reporter-version.ts
2922
- var fs4 = __toESM(require("fs"));
2923
- var path6 = __toESM(require("path"));
3430
+ var fs5 = __toESM(require("fs"));
3431
+ var path7 = __toESM(require("path"));
2924
3432
  var CANDIDATE_OFFSETS = ["..", "../../.."];
2925
3433
  function findOwnPackageJson(fromDir) {
2926
3434
  for (const offset of CANDIDATE_OFFSETS) {
2927
3435
  try {
2928
- const pkgPath = path6.resolve(fromDir, offset, "package.json");
2929
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
3436
+ const pkgPath = path7.resolve(fromDir, offset, "package.json");
3437
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
2930
3438
  const { name, version } = pkg;
2931
3439
  if (name === "@piwitests/reporter" && typeof version === "string")
2932
- return { version, root: path6.dirname(pkgPath) };
3440
+ return { version, root: path7.dirname(pkgPath) };
2933
3441
  } catch {
2934
3442
  }
2935
3443
  }
@@ -2962,7 +3470,7 @@ function resolveBoxing() {
2962
3470
  if (typeof set !== "function") continue;
2963
3471
  return {
2964
3472
  set,
2965
- prefixes: [path7.dirname(playwrightPackageJson), path7.join(root, "dist") + path7.sep]
3473
+ prefixes: [path8.dirname(playwrightPackageJson), path8.join(root, "dist") + path8.sep]
2966
3474
  };
2967
3475
  }
2968
3476
  } catch {
@@ -2987,6 +3495,443 @@ function internalCall(owner, run) {
2987
3495
  return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
2988
3496
  }
2989
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
+ }
3933
+ }
3934
+
2990
3935
  // src/internal/capture/capture-fixtures.ts
2991
3936
  function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
2992
3937
  const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
@@ -3036,11 +3981,53 @@ function createSink() {
3036
3981
  pageInventories: [],
3037
3982
  probeItem: null,
3038
3983
  probeInterception: null,
3984
+ flake: null,
3039
3985
  pickOffered: false,
3040
3986
  userPick: null,
3041
- locatorPages: new LocatorPageLog()
3987
+ locatorPages: new LocatorPageLog(),
3988
+ codeReach: null
3042
3989
  };
3043
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
+ }
3044
4031
  var currentSink = null;
3045
4032
  var PENDING_PROBES = /* @__PURE__ */ new Set();
3046
4033
  async function drainPendingProbes(capMs) {
@@ -3048,8 +4035,8 @@ async function drainPendingProbes(capMs) {
3048
4035
  let cap;
3049
4036
  await Promise.race([
3050
4037
  Promise.allSettled(PENDING_PROBES),
3051
- new Promise((resolve2) => {
3052
- cap = setTimeout(resolve2, capMs);
4038
+ new Promise((resolve3) => {
4039
+ cap = setTimeout(resolve3, capMs);
3053
4040
  })
3054
4041
  ]);
3055
4042
  clearTimeout(cap);
@@ -3089,10 +4076,10 @@ async function readWebVitals(page) {
3089
4076
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
3090
4077
  paint[key] = Math.round(entry.startTime);
3091
4078
  }
3092
- const readBuffered = (type, extra) => new Promise((resolve2) => {
4079
+ const readBuffered = (type, extra) => new Promise((resolve3) => {
3093
4080
  try {
3094
4081
  const PO = globalThis.PerformanceObserver;
3095
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve2(null);
4082
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve3(null);
3096
4083
  const out = [];
3097
4084
  const po = new PO((list) => out.push(...list.getEntries()));
3098
4085
  po.observe({ type, buffered: true, ...extra });
@@ -3102,10 +4089,10 @@ async function readWebVitals(page) {
3102
4089
  po.disconnect();
3103
4090
  } catch {
3104
4091
  }
3105
- resolve2(out);
4092
+ resolve3(out);
3106
4093
  }, 0);
3107
4094
  } catch {
3108
- resolve2(null);
4095
+ resolve3(null);
3109
4096
  }
3110
4097
  });
3111
4098
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -3218,12 +4205,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
3218
4205
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
3219
4206
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
3220
4207
  function withTimeout(promise, ms) {
3221
- return new Promise((resolve2, reject) => {
4208
+ return new Promise((resolve3, reject) => {
3222
4209
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
3223
4210
  promise.then(
3224
4211
  (value) => {
3225
4212
  clearTimeout(timer);
3226
- resolve2(value);
4213
+ resolve3(value);
3227
4214
  },
3228
4215
  (error) => {
3229
4216
  clearTimeout(timer);
@@ -3550,9 +4537,11 @@ function wrapFrameLocator(page, frameLocator) {
3550
4537
  }
3551
4538
  });
3552
4539
  }
4540
+ var CANCELLED_REQUEST = /^net::ERR_ABORTED$|^NS_BINDING_ABORTED$|^NS_ERROR_ABORT$|cancell?ed/i;
3553
4541
  function instrumentPage(page) {
3554
4542
  if (!page || INSTRUMENTED_PAGES.has(page)) return;
3555
4543
  INSTRUMENTED_PAGES.add(page);
4544
+ void maybeStartCodeReach(page);
3556
4545
  const probeSink = currentSink;
3557
4546
  if (probeSink?.probeItem && !probeSink.probeInterception) {
3558
4547
  void installProbeInterception(page, probeSink.probeItem).then((interception) => {
@@ -3560,6 +4549,18 @@ function instrumentPage(page) {
3560
4549
  }).catch(() => {
3561
4550
  });
3562
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
+ }
3563
4564
  const ctx = pageContext(page);
3564
4565
  if (ctx) instrumentContext(ctx);
3565
4566
  if (typeof page.close === "function") {
@@ -3570,6 +4571,7 @@ function instrumentPage(page) {
3570
4571
  if (sink) {
3571
4572
  await stashPageState(sink, { page });
3572
4573
  await maybeOpenPicker(sink, { page });
4574
+ await stopSinkCodeReach(sink, { page });
3573
4575
  }
3574
4576
  return originalClose(...args);
3575
4577
  };
@@ -3641,7 +4643,7 @@ function instrumentPage(page) {
3641
4643
  } catch {
3642
4644
  }
3643
4645
  }
3644
- page.on("requestfinished", (request) => {
4646
+ const recordRequest = (request, failed) => {
3645
4647
  const sink = currentSink;
3646
4648
  if (!sink) return;
3647
4649
  sink.lastActivePage = page;
@@ -3649,18 +4651,21 @@ function instrumentPage(page) {
3649
4651
  try {
3650
4652
  const url = request.url();
3651
4653
  if (url.startsWith("data:") || url.startsWith("blob:")) return;
3652
- const timing = request.timing();
3653
- const response = await request.response();
3654
4654
  const resourceType = request.resourceType();
3655
- 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();
3656
4660
  const entry = {
3657
4661
  method: request.method(),
3658
4662
  url,
3659
4663
  status: response ? response.status() : 0,
3660
- duration: timing.responseEnd > 0 ? Math.round(timing.responseEnd - timing.requestStart) : 0,
4664
+ duration: requestDuration(timing, failed),
3661
4665
  startTime: timing.startTime,
3662
4666
  resourceType
3663
4667
  };
4668
+ if (failure) entry.failure = failure;
3664
4669
  if (response) {
3665
4670
  const headers = response.headers();
3666
4671
  const contentType = headers["content-type"];
@@ -3685,7 +4690,12 @@ function instrumentPage(page) {
3685
4690
  }
3686
4691
  })();
3687
4692
  sink.pendingHandlers.push(p);
3688
- });
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;
3689
4699
  }
3690
4700
  function instrumentContext(context) {
3691
4701
  if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
@@ -3698,6 +4708,8 @@ function instrumentContext(context) {
3698
4708
  context.newPage = async (...args) => {
3699
4709
  const page = await originalNewPage(...args);
3700
4710
  instrumentPage(page);
4711
+ await awaitFlakeConditions();
4712
+ await maybeStartCodeReach(page);
3701
4713
  return page;
3702
4714
  };
3703
4715
  if (typeof context.close === "function") {
@@ -3708,6 +4720,7 @@ function instrumentContext(context) {
3708
4720
  if (sink) {
3709
4721
  await stashPageState(sink, { context });
3710
4722
  await maybeOpenPicker(sink, { context });
4723
+ await stopSinkCodeReach(sink, { context });
3711
4724
  }
3712
4725
  return originalClose(...args);
3713
4726
  };
@@ -3721,6 +4734,8 @@ function patchBrowser(browser) {
3721
4734
  browser.newPage = async (...args) => {
3722
4735
  const page = await originalNewPage(...args);
3723
4736
  instrumentPage(page);
4737
+ await awaitFlakeConditions();
4738
+ await maybeStartCodeReach(page);
3724
4739
  return page;
3725
4740
  };
3726
4741
  const originalNewContext = browser.newContext.bind(browser);
@@ -3743,13 +4758,19 @@ function readRootAria(page) {
3743
4758
  function readRootAriaJson(page) {
3744
4759
  return internalCall(page, () => ariaSnapshotJSONBestEffort(page.locator(":root")));
3745
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
+ }
3746
4767
  async function flushSink(sink, testInfo) {
3747
4768
  await Promise.allSettled(sink.pendingHandlers);
3748
4769
  let drainDeadline;
3749
4770
  await Promise.race([
3750
4771
  Promise.allSettled(sink.capturePromises),
3751
- new Promise((resolve2) => {
3752
- drainDeadline = setTimeout(resolve2, 2e3);
4772
+ new Promise((resolve3) => {
4773
+ drainDeadline = setTimeout(resolve3, 2e3);
3753
4774
  })
3754
4775
  ]);
3755
4776
  clearTimeout(drainDeadline);
@@ -3765,6 +4786,13 @@ async function flushSink(sink, testInfo) {
3765
4786
  body: Buffer.from(JSON.stringify(dedupeSnapshotsByLocation(sink.capturedLocators)))
3766
4787
  });
3767
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
+ }
3768
4796
  if (sink.locatorPages.size > 0) {
3769
4797
  await testInfo.attach(ATTACHMENT_NAMES.locatorPages, {
3770
4798
  contentType: "application/json",
@@ -3898,6 +4926,8 @@ var piwiFixtures = {
3898
4926
  // never double-wraps a page the browser patch already instrumented.
3899
4927
  page: async ({ page }, use) => {
3900
4928
  instrumentPage(page);
4929
+ await awaitFlakeConditions();
4930
+ await maybeStartCodeReach(page);
3901
4931
  await use(page);
3902
4932
  },
3903
4933
  // Auto, test-scoped: open a capture sink for the running test and flush it
@@ -3906,8 +4936,23 @@ var piwiFixtures = {
3906
4936
  piwiCapture: [
3907
4937
  async ({}, use, testInfo) => {
3908
4938
  boxCaptureFrames();
4939
+ const conflict = labModeConflict();
4940
+ if (conflict) throw new Error(conflict);
3909
4941
  const sink = createSink();
3910
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
+ }
3911
4956
  if (isProbeMode())
3912
4957
  sink.probeItem = probeItemForTest({
3913
4958
  title: testInfo.title,
@@ -3938,12 +4983,34 @@ var piwiFixtures = {
3938
4983
  handled
3939
4984
  });
3940
4985
  }
4986
+ if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
3941
4987
  await flushSink(sink, testInfo);
3942
4988
  }
3943
4989
  },
3944
4990
  { auto: true }
3945
4991
  ]
3946
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
+ }
3947
5014
  function extendPiwiFixtures(test) {
3948
5015
  return test.extend(piwiFixtures);
3949
5016
  }
@@ -3956,11 +5023,17 @@ function extendPiwiFixtures(test) {
3956
5023
  boxCaptureFrames,
3957
5024
  buildPageState,
3958
5025
  callMethod,
5026
+ codeReachRoots,
3959
5027
  computeCoreVitals,
5028
+ configuredCodeReachRoots,
3960
5029
  extendPiwiFixtures,
3961
5030
  internalCall,
3962
5031
  noteLocatorCall,
3963
5032
  noteNavigation,
5033
+ pageMapFetcher,
3964
5034
  piwiFixtures,
3965
- probeElementAttrs
5035
+ probeElementAttrs,
5036
+ resolveCodeReach,
5037
+ startCodeReach,
5038
+ stopCodeReach
3966
5039
  });