@piwitests/reporter 0.40.0 → 0.42.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,478 @@ 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
+ var SELECTOR_OPTION_RE = /\b(name|hasText|hasNotText|has|placeholder|label|title|alt|exact)\s*:\s*(['"`])(?:\\.|(?!\2)[\s\S])*?\2/gi;
2817
+ function classifyError(text) {
2818
+ if (/strict mode violation/i.test(text)) return "strict-mode";
2819
+ if (/\bexpect\(|\bexpect\.|Expected (?:string|substring|pattern|value)|\.toHave|\.toBe|\.toContain|\.toEqual/.test(text))
2820
+ return "assertion";
2821
+ if (/Target page, context or browser has been closed|Target closed|browser has been closed|Page crashed/i.test(text))
2822
+ return "crash";
2823
+ if (/net::ERR_|NS_ERROR_|Navigation failed/i.test(text)) return "navigation";
2824
+ if (/Timeout \d+m?s exceeded|TimeoutError|Timed out \d+m?s/i.test(text)) return "timeout";
2825
+ return "unknown";
2826
+ }
2827
+ function maskVolatile(text) {
2828
+ 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>");
2829
+ }
2830
+ function maskSelectorOptions(text) {
2831
+ return text.replace(SELECTOR_OPTION_RE, (_m, key) => `${key}: <STR>`);
2832
+ }
2833
+ function extractErrorSignature(rawError) {
2834
+ const text = stripAnsi(rawError);
2835
+ const errorType = classifyError(text);
2836
+ const normalizedMessage = maskVolatile(maskSelectorOptions(extractMessageHead(text)));
2837
+ const selector = extractSelector(text);
2838
+ const topFrameFile = extractTopFrameFile(text);
2839
+ const signature = (normalizedMessage.split("\n")[0] || "").slice(0, 200) || "Unknown error";
2840
+ return { errorType, signature, normalizedMessage, selector, topFrameFile };
2841
+ }
2842
+
2843
+ // ../core/src/flake-plan.ts
2844
+ var FLAKE_PLAN_VERSION = 1;
2845
+ var FLAKE_CONDITION_KINDS = [
2846
+ "delay",
2847
+ "fail",
2848
+ "cpu",
2849
+ "network",
2850
+ "alongside",
2851
+ "after",
2852
+ "project"
2853
+ ];
2854
+ var COMMAND_CONDITION_KINDS = ["alongside", "after", "project"];
2855
+ function flakeErrorSignature(rawError) {
2856
+ return extractErrorSignature(rawError).normalizedMessage;
2857
+ }
2858
+ var FlakePlanError = class extends Error {
2859
+ constructor(message) {
2860
+ super(`Invalid flake plan: ${message}`);
2861
+ this.name = "FlakePlanError";
2862
+ }
2863
+ };
2864
+ function isObject(value) {
2865
+ return !!value && typeof value === "object" && !Array.isArray(value);
2866
+ }
2867
+ function str(obj, key, at) {
2868
+ const value = obj[key];
2869
+ if (typeof value !== "string" || !value.trim()) throw new FlakePlanError(`${at}.${key} must be a non-empty string`);
2870
+ return value;
2871
+ }
2872
+ function num(obj, key, at, min, max = Number.MAX_SAFE_INTEGER) {
2873
+ const value = obj[key];
2874
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
2875
+ throw new FlakePlanError(`${at}.${key} must be a number from ${min} to ${max}`);
2876
+ }
2877
+ return value;
2878
+ }
2879
+ function stringList(value, at) {
2880
+ if (value === void 0) return [];
2881
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
2882
+ throw new FlakePlanError(`${at} must be a list of strings`);
2883
+ }
2884
+ return value;
2885
+ }
2886
+ var ROUTE_KEY_RE = /^[A-Z]+ \/\S*$/;
2887
+ function routeKey(obj, at) {
2888
+ const route = str(obj, "route", at);
2889
+ if (!ROUTE_KEY_RE.test(route)) {
2890
+ throw new FlakePlanError(`${at}.route must be a method and a path, like "GET /api/cart/:id" (got "${route}")`);
2891
+ }
2892
+ return route;
2893
+ }
2894
+ function routeMatch(obj, at) {
2895
+ const value = obj.match;
2896
+ if (value === void 0 || value === "all") return "all";
2897
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
2898
+ throw new FlakePlanError(`${at}.match must be "all" or a whole number from 1`);
2899
+ }
2900
+ function testRef(value, at) {
2901
+ if (!isObject(value)) throw new FlakePlanError(`${at} must be an object`);
2902
+ return {
2903
+ file: str(value, "file", at),
2904
+ title: str(value, "title", at),
2905
+ suite: stringList(value.suite, `${at}.suite`)
2906
+ };
2907
+ }
2908
+ function parseCondition(value, at) {
2909
+ if (!isObject(value)) throw new FlakePlanError(`${at} must be an object`);
2910
+ const kind = value.kind;
2911
+ switch (kind) {
2912
+ case "delay":
2913
+ return { kind, route: routeKey(value, at), ms: num(value, "ms", at, 0, 6e5), match: routeMatch(value, at) };
2914
+ case "fail": {
2915
+ const route = routeKey(value, at);
2916
+ const match = routeMatch(value, at);
2917
+ if (value.abort === true) {
2918
+ if (value.status !== void 0) throw new FlakePlanError(`${at} sets both status and abort; choose one`);
2919
+ return { kind, route, abort: true, match };
2920
+ }
2921
+ const status = num(value, "status", at, 100, 599);
2922
+ if (!Number.isInteger(status)) throw new FlakePlanError(`${at}.status must be a whole number`);
2923
+ return { kind, route, status, match };
2924
+ }
2925
+ case "cpu":
2926
+ return { kind, rate: num(value, "rate", at, 1, 100) };
2927
+ case "network":
2928
+ return {
2929
+ kind,
2930
+ latencyMs: num(value, "latencyMs", at, 0, 6e4),
2931
+ downKbps: num(value, "downKbps", at, 1),
2932
+ upKbps: num(value, "upKbps", at, 1)
2933
+ };
2934
+ case "alongside":
2935
+ case "after":
2936
+ return { kind, test: testRef(value.test, `${at}.test`) };
2937
+ case "project":
2938
+ return { kind, name: str(value, "name", at) };
2939
+ default:
2940
+ throw new FlakePlanError(
2941
+ `${at}.kind ${JSON.stringify(kind)} is not a condition this version knows (${FLAKE_CONDITION_KINDS.join(", ")})`
2942
+ );
2943
+ }
2944
+ }
2945
+ function parseFlakePlan(value) {
2946
+ if (!isObject(value)) throw new FlakePlanError("the plan must be a JSON object");
2947
+ if (value.version !== FLAKE_PLAN_VERSION) {
2948
+ throw new FlakePlanError(`version must be ${FLAKE_PLAN_VERSION} (got ${JSON.stringify(value.version)})`);
2949
+ }
2950
+ const experimentId = str(value, "experimentId", "plan");
2951
+ if (!isObject(value.test)) throw new FlakePlanError("plan.test must be an object");
2952
+ const ref = testRef(value.test, "plan.test");
2953
+ const project = value.test.project;
2954
+ if (project !== void 0 && project !== null && (typeof project !== "string" || !project.trim())) {
2955
+ throw new FlakePlanError("plan.test.project must be a non-empty string or null");
2956
+ }
2957
+ if (!isObject(value.arm)) throw new FlakePlanError("plan.arm must be an object");
2958
+ const armId = str(value.arm, "id", "plan.arm");
2959
+ const rawConditions = value.arm.conditions ?? [];
2960
+ if (!Array.isArray(rawConditions)) throw new FlakePlanError("plan.arm.conditions must be a list");
2961
+ const conditions = rawConditions.map((c, i) => parseCondition(c, `plan.arm.conditions[${i}]`));
2962
+ return {
2963
+ version: FLAKE_PLAN_VERSION,
2964
+ experimentId,
2965
+ test: { ...ref, project: typeof project === "string" ? project : null },
2966
+ arm: { id: armId, conditions },
2967
+ errorSignatures: stringList(value.errorSignatures, "plan.errorSignatures")
2968
+ };
2969
+ }
2970
+ function parseFlakePlanText(text) {
2971
+ let json;
2972
+ try {
2973
+ json = JSON.parse(text);
2974
+ } catch (error) {
2975
+ throw new FlakePlanError(`not valid JSON (${error instanceof Error ? error.message : String(error)})`);
2976
+ }
2977
+ return parseFlakePlan(json);
2978
+ }
2979
+ function suiteEqual(a, b) {
2980
+ return !!b && a.length === b.length && a.every((title, i) => title === b[i]);
2981
+ }
2982
+ function flakeTestMatches(ref, test) {
2983
+ if (!test.file || ref.file !== test.file || ref.title !== test.title) return false;
2984
+ return test.suite === void 0 || ref.suite.length === 0 || suiteEqual(ref.suite, test.suite);
2985
+ }
2986
+
2987
+ // src/internal/support/errors.ts
2988
+ function errorMessage(error) {
2989
+ if (error instanceof Error) return error.message;
2990
+ return String(error);
2991
+ }
2992
+
2993
+ // src/internal/flake/mode.ts
2994
+ function isFlakeMode() {
2995
+ return !!process.env[PIWI_FLAKE_ENV.plan]?.trim();
2996
+ }
2997
+ function labModeConflict() {
2998
+ if (!isProbeMode() || !isFlakeMode()) return null;
2999
+ 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.`;
3000
+ }
3001
+ var planCache2 = null;
3002
+ function loadFlakePlan() {
3003
+ const file = process.env[PIWI_FLAKE_ENV.plan]?.trim() ?? "";
3004
+ if (planCache2 && planCache2.file === file) return planCache2.plan;
3005
+ let text;
3006
+ try {
3007
+ text = fs3.readFileSync(file, "utf8");
3008
+ } catch (error) {
3009
+ throw new Error(`[Piwi Dashboard] Cannot read the flake plan ${file}: ${errorMessage(error)}`);
3010
+ }
3011
+ try {
3012
+ const plan = parseFlakePlanText(text);
3013
+ planCache2 = { file, plan };
3014
+ return plan;
3015
+ } catch (error) {
3016
+ throw new Error(`[Piwi Dashboard] ${errorMessage(error)} (${file})`);
3017
+ }
3018
+ }
3019
+ function relativeTestFile(file) {
3020
+ return file ? path4.relative(process.cwd(), file).split(path4.sep).join("/") : null;
3021
+ }
3022
+ function flakeRoleForTest(plan, test) {
3023
+ const running = {
3024
+ file: relativeTestFile(test.file),
3025
+ title: test.title,
3026
+ suite: test.titlePath ? test.titlePath.slice(1, -1) : void 0
3027
+ };
3028
+ if (flakeTestMatches(plan.test, running) && (plan.test.project === null || plan.test.project === test.project)) {
3029
+ return "target";
3030
+ }
3031
+ const companion = plan.arm.conditions.some(
3032
+ (c) => (c.kind === "alongside" || c.kind === "after") && flakeTestMatches(c.test, running)
3033
+ );
3034
+ return companion ? "companion" : null;
3035
+ }
3036
+ function pageBrowserName(page) {
3037
+ try {
3038
+ return page.context().browser()?.browserType().name() ?? null;
3039
+ } catch {
3040
+ return null;
3041
+ }
3042
+ }
3043
+ function cdpCommandFor(condition) {
3044
+ if (condition.kind === "cpu") return { method: "Emulation.setCPUThrottlingRate", params: { rate: condition.rate } };
3045
+ if (condition.kind === "network") {
3046
+ return {
3047
+ method: "Network.emulateNetworkConditions",
3048
+ params: {
3049
+ offline: false,
3050
+ latency: condition.latencyMs,
3051
+ // Kilobits per second to bytes per second.
3052
+ downloadThroughput: condition.downKbps * 1e3 / 8,
3053
+ uploadThroughput: condition.upKbps * 1e3 / 8
3054
+ }
3055
+ };
3056
+ }
3057
+ return null;
3058
+ }
3059
+ function conditionRouteAction(condition) {
3060
+ if (condition.kind === "delay") return { kind: "delay", ms: condition.ms };
3061
+ if (condition.kind === "fail")
3062
+ return "abort" in condition ? { kind: "abort" } : { kind: "status", status: condition.status };
3063
+ return null;
3064
+ }
3065
+ function unappliedReports(plan) {
3066
+ return plan.arm.conditions.map((c) => {
3067
+ if (COMMAND_CONDITION_KINDS.includes(c.kind)) return { kind: c.kind, outcome: "by-command" };
3068
+ if (cdpCommandFor(c)) return { kind: c.kind, outcome: "skipped", note: "the attempt opened no page" };
3069
+ return { kind: c.kind, outcome: "not-matched" };
3070
+ });
3071
+ }
3072
+ async function installFlakeConditions(page, plan) {
3073
+ const conditions = plan.arm.conditions;
3074
+ const reports = unappliedReports(plan);
3075
+ const rules = [];
3076
+ conditions.forEach((condition, i) => {
3077
+ const action = conditionRouteAction(condition);
3078
+ if (!action || condition.kind !== "delay" && condition.kind !== "fail") return;
3079
+ const route = condition.route;
3080
+ rules.push({
3081
+ matches: (method, url) => requestRouteKey(method, url) === route,
3082
+ match: condition.match,
3083
+ handle: async (r, startedAt) => {
3084
+ reports[i] = { kind: condition.kind, outcome: "applied" };
3085
+ try {
3086
+ await performRouteAction(r, action, startedAt);
3087
+ } catch {
3088
+ await r.fallback().catch(() => {
3089
+ });
3090
+ }
3091
+ }
3092
+ });
3093
+ });
3094
+ await installRouteRules(page, rules);
3095
+ const cdp = conditions.map((c, i) => ({ i, command: cdpCommandFor(c) })).filter((c) => c.command);
3096
+ if (cdp.length > 0) {
3097
+ const browserName = pageBrowserName(page);
3098
+ if (browserName !== null && browserName !== "chromium") {
3099
+ for (const { i } of cdp) {
3100
+ reports[i] = {
3101
+ kind: conditions[i].kind,
3102
+ outcome: "skipped",
3103
+ note: `needs Chromium; this attempt ran in ${browserName}`
3104
+ };
3105
+ }
3106
+ } else {
3107
+ try {
3108
+ const session = await page.context().newCDPSession(page);
3109
+ for (const { i, command } of cdp) {
3110
+ try {
3111
+ if (command.method.startsWith("Network.")) await session.send("Network.enable");
3112
+ await session.send(command.method, command.params);
3113
+ reports[i] = { kind: conditions[i].kind, outcome: "applied" };
3114
+ } catch (error) {
3115
+ reports[i] = { kind: conditions[i].kind, outcome: "skipped", note: errorMessage(error) };
3116
+ }
3117
+ }
3118
+ } catch (error) {
3119
+ for (const { i } of cdp) {
3120
+ reports[i] = {
3121
+ kind: conditions[i].kind,
3122
+ outcome: "skipped",
3123
+ note: `no DevTools session: ${errorMessage(error)}`
3124
+ };
3125
+ }
3126
+ }
3127
+ }
3128
+ }
3129
+ return { reports: () => reports.map((r) => ({ ...r })) };
3130
+ }
3131
+ var FAILED_STATUSES = /* @__PURE__ */ new Set(["failed", "timedOut", "interrupted"]);
3132
+ function flakeResultLine(plan, attempt) {
3133
+ const failed = FAILED_STATUSES.has(attempt.status);
3134
+ const errorSignature = failed && attempt.errorText ? flakeErrorSignature(attempt.errorText) : null;
3135
+ return {
3136
+ version: FLAKE_PLAN_VERSION,
3137
+ experimentId: plan.experimentId,
3138
+ armId: plan.arm.id,
3139
+ role: attempt.role,
3140
+ file: relativeTestFile(attempt.file),
3141
+ title: attempt.title,
3142
+ project: attempt.project,
3143
+ browserName: attempt.browserName,
3144
+ status: attempt.status,
3145
+ errorSignature,
3146
+ matchesHistory: errorSignature !== null && plan.errorSignatures.includes(errorSignature),
3147
+ startedAt: attempt.startedAt,
3148
+ duration: attempt.duration,
3149
+ workerIndex: attempt.workerIndex,
3150
+ parallelIndex: attempt.parallelIndex,
3151
+ repeatEachIndex: attempt.repeatEachIndex,
3152
+ retry: attempt.retry,
3153
+ conditions: attempt.role === "target" ? attempt.conditions : []
3154
+ };
3155
+ }
3156
+ function recordFlakeResult(line) {
3157
+ const file = process.env[PIWI_FLAKE_ENV.results]?.trim();
3158
+ if (!file) return;
3159
+ try {
3160
+ fs3.appendFileSync(file, `${JSON.stringify(line)}
3161
+ `);
3162
+ } catch {
3163
+ }
3164
+ }
3165
+
3166
+ // ../core/src/error-text.ts
3167
+ function joinErrorMessages(errors) {
3168
+ if (!errors || errors.length === 0) return null;
3169
+ const seen = /* @__PURE__ */ new Set();
3170
+ const parts = [];
3171
+ for (const err of errors) {
3172
+ const msg = err.message ?? "";
3173
+ if (!msg) continue;
3174
+ const dedupeKey = msg.trim().slice(0, 200);
3175
+ if (seen.has(dedupeKey)) continue;
3176
+ seen.add(dedupeKey);
3177
+ parts.push(msg);
3178
+ }
3179
+ return parts.join("\n---\n") || null;
3180
+ }
3181
+
3182
+ // src/internal/capture/inspect-on-failure.ts
3183
+ function isCi(ci) {
3184
+ return ci !== void 0 && ci !== "" && ci !== "false";
3185
+ }
3186
+ function shouldInspectOnFailure(gate) {
3187
+ if (gate.enabled !== "true") return false;
3188
+ if (isCi(gate.ci)) return false;
3189
+ if (gate.headless !== false) return false;
3190
+ if (gate.status !== "failed" && gate.status !== "timedOut") return false;
3191
+ if (gate.status === gate.expectedStatus) return false;
3192
+ return gate.retry >= gate.retries;
3193
+ }
3194
+ function environmentalSkipReason(gate) {
3195
+ if (gate.enabled !== "true") return null;
3196
+ if (gate.status !== "failed" && gate.status !== "timedOut") return null;
3197
+ if (gate.status === gate.expectedStatus) return null;
3198
+ if (isCi(gate.ci)) return "running under CI \u2014 this is a headed, local-only feature";
3199
+ if (gate.headless !== false) {
3200
+ return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
3201
+ }
3202
+ return null;
3203
+ }
3204
+ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT_ON_FAIL) {
3205
+ const use = testInfo.project?.use ?? {};
3206
+ return {
3207
+ enabled,
3208
+ ci: process.env.CI,
3209
+ status: testInfo.status,
3210
+ expectedStatus: testInfo.expectedStatus,
3211
+ headless: use.headless,
3212
+ retry: testInfo.retry,
3213
+ retries: testInfo.project?.retries ?? 0
3214
+ };
3215
+ }
3216
+
2706
3217
  // src/internal/capture/pick-on-failure.ts
2707
- var ANSI_RE = /\[[0-9;]*m/g;
3218
+ var path5 = __toESM(require("path"));
3219
+ var ANSI_RE2 = /\[[0-9;]*m/g;
2708
3220
  function parseLeafLocatorExpression(rawExpr) {
2709
3221
  const leaf = parseLeafLocatorCall(rawExpr);
2710
3222
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -2714,13 +3226,13 @@ function deriveFailedLocator(testInfo) {
2714
3226
  const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
2715
3227
  for (const err of errors) {
2716
3228
  const text = `${err.message ?? ""}
2717
- ${err.stack ?? ""}`.replace(ANSI_RE, "");
3229
+ ${err.stack ?? ""}`.replace(ANSI_RE2, "");
2718
3230
  const line = /^\s*Locator:\s*(.+)$/m.exec(text);
2719
3231
  if (!line) continue;
2720
3232
  const parsed = parseLeafLocatorExpression(line[1].trim());
2721
3233
  if (!parsed) continue;
2722
3234
  const loc = err.location;
2723
- const location = loc ? `${path4.relative(process.cwd(), loc.file).split(path4.sep).join("/")}:${loc.line}:${loc.column}` : null;
3235
+ const location = loc ? `${path5.relative(process.cwd(), loc.file).split(path5.sep).join("/")}:${loc.line}:${loc.column}` : null;
2724
3236
  return { method: parsed.method, args: parsed.args, location };
2725
3237
  }
2726
3238
  return null;
@@ -2869,9 +3381,9 @@ function applyPickToSnapshots(snapshots, pick) {
2869
3381
  }
2870
3382
 
2871
3383
  // src/internal/support/aria-sampling.ts
2872
- var path5 = __toESM(require("path"));
3384
+ var path6 = __toESM(require("path"));
2873
3385
  var os3 = __toESM(require("os"));
2874
- var fs3 = __toESM(require("fs"));
3386
+ var fs4 = __toESM(require("fs"));
2875
3387
 
2876
3388
  // src/internal/support/instance-id.ts
2877
3389
  var crypto = __toESM(require("crypto"));
@@ -2885,14 +3397,14 @@ function ariaSampleIdentity(filePath, title) {
2885
3397
  return `${filePath}\0${title}`;
2886
3398
  }
2887
3399
  function getAriaSampleFilePath(projectName) {
2888
- return path5.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
3400
+ return path6.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
2889
3401
  }
2890
3402
  var cachedSets = /* @__PURE__ */ new Map();
2891
3403
  function loadAriaSampleSet(projectName) {
2892
3404
  if (cachedSets.has(projectName)) return cachedSets.get(projectName);
2893
3405
  let set = null;
2894
3406
  try {
2895
- const raw = fs3.readFileSync(getAriaSampleFilePath(projectName), "utf8");
3407
+ const raw = fs4.readFileSync(getAriaSampleFilePath(projectName), "utf8");
2896
3408
  const parsed = JSON.parse(raw);
2897
3409
  if (parsed.projectName === projectName && Array.isArray(parsed.identities)) {
2898
3410
  set = new Set(parsed.identities.filter((x) => typeof x === "string"));
@@ -2903,33 +3415,33 @@ function loadAriaSampleSet(projectName) {
2903
3415
  cachedSets.set(projectName, set);
2904
3416
  return set;
2905
3417
  }
2906
- function relativeTestFile(file) {
2907
- return path5.relative(process.cwd(), file).split(path5.sep).join("/");
3418
+ function relativeTestFile2(file) {
3419
+ return path6.relative(process.cwd(), file).split(path6.sep).join("/");
2908
3420
  }
2909
3421
  function isDueForAriaSample(testInfo) {
2910
3422
  const projectName = process.env.PIWI_PROJECT_NAME;
2911
3423
  if (!projectName) return false;
2912
3424
  const set = loadAriaSampleSet(projectName);
2913
3425
  if (!set || set.size === 0) return false;
2914
- return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
3426
+ return set.has(ariaSampleIdentity(relativeTestFile2(testInfo.file), testInfo.title));
2915
3427
  }
2916
3428
 
2917
3429
  // src/internal/capture/quiet-capture.ts
2918
3430
  var import_node_module = require("module");
2919
- var path7 = __toESM(require("path"));
3431
+ var path8 = __toESM(require("path"));
2920
3432
 
2921
3433
  // src/internal/support/reporter-version.ts
2922
- var fs4 = __toESM(require("fs"));
2923
- var path6 = __toESM(require("path"));
3434
+ var fs5 = __toESM(require("fs"));
3435
+ var path7 = __toESM(require("path"));
2924
3436
  var CANDIDATE_OFFSETS = ["..", "../../.."];
2925
3437
  function findOwnPackageJson(fromDir) {
2926
3438
  for (const offset of CANDIDATE_OFFSETS) {
2927
3439
  try {
2928
- const pkgPath = path6.resolve(fromDir, offset, "package.json");
2929
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
3440
+ const pkgPath = path7.resolve(fromDir, offset, "package.json");
3441
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
2930
3442
  const { name, version } = pkg;
2931
3443
  if (name === "@piwitests/reporter" && typeof version === "string")
2932
- return { version, root: path6.dirname(pkgPath) };
3444
+ return { version, root: path7.dirname(pkgPath) };
2933
3445
  } catch {
2934
3446
  }
2935
3447
  }
@@ -2962,7 +3474,7 @@ function resolveBoxing() {
2962
3474
  if (typeof set !== "function") continue;
2963
3475
  return {
2964
3476
  set,
2965
- prefixes: [path7.dirname(playwrightPackageJson), path7.join(root, "dist") + path7.sep]
3477
+ prefixes: [path8.dirname(playwrightPackageJson), path8.join(root, "dist") + path8.sep]
2966
3478
  };
2967
3479
  }
2968
3480
  } catch {
@@ -2987,6 +3499,443 @@ function internalCall(owner, run) {
2987
3499
  return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
2988
3500
  }
2989
3501
 
3502
+ // src/internal/capture/code-reach.ts
3503
+ var crypto2 = __toESM(require("crypto"));
3504
+ var fs6 = __toESM(require("fs"));
3505
+ var path9 = __toESM(require("path"));
3506
+ var import_node_child_process = require("child_process");
3507
+
3508
+ // ../core/src/source-map.ts
3509
+ var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3510
+ var BASE64_VALUE = new Int8Array(128).fill(-1);
3511
+ for (let i = 0; i < BASE64.length; i++) BASE64_VALUE[BASE64.charCodeAt(i)] = i;
3512
+ function decodeMappings(mappings, sourceBase) {
3513
+ const lines = [];
3514
+ let current = { columns: [], sources: [] };
3515
+ let column = 0;
3516
+ let source = 0;
3517
+ const fields = [];
3518
+ let value = 0;
3519
+ let shift = 0;
3520
+ const flush = () => {
3521
+ if (fields.length === 0) return;
3522
+ column += fields[0];
3523
+ if (fields.length >= 4) {
3524
+ source += fields[1];
3525
+ current.columns.push(column);
3526
+ current.sources.push(sourceBase + source);
3527
+ } else {
3528
+ current.columns.push(column);
3529
+ current.sources.push(-1);
3530
+ }
3531
+ fields.length = 0;
3532
+ };
3533
+ for (let i = 0; i <= mappings.length; i++) {
3534
+ const ch = i < mappings.length ? mappings.charCodeAt(i) : 59;
3535
+ if (ch === 59 || ch === 44) {
3536
+ flush();
3537
+ if (ch === 59) {
3538
+ lines.push(current);
3539
+ current = { columns: [], sources: [] };
3540
+ column = 0;
3541
+ }
3542
+ continue;
3543
+ }
3544
+ const digit = ch < 128 ? BASE64_VALUE[ch] : -1;
3545
+ if (digit < 0) continue;
3546
+ value += (digit & 31) << shift;
3547
+ if (digit & 32) {
3548
+ shift += 5;
3549
+ continue;
3550
+ }
3551
+ fields.push(value & 1 ? -(value >>> 1) : value >>> 1);
3552
+ value = 0;
3553
+ shift = 0;
3554
+ }
3555
+ for (const line of lines) {
3556
+ if (line.columns.every((c, i) => i === 0 || c >= line.columns[i - 1])) continue;
3557
+ const order = line.columns.map((_, i) => i).sort((a, b) => line.columns[a] - line.columns[b]);
3558
+ line.columns = order.map((i) => line.columns[i]);
3559
+ line.sources = order.map((i) => line.sources[i]);
3560
+ }
3561
+ return lines;
3562
+ }
3563
+ function joinRoot(root, source) {
3564
+ if (!root || /^[a-z][\w+.-]*:/i.test(source) || source.startsWith("/")) return source;
3565
+ return root.endsWith("/") ? root + source : `${root}/${source}`;
3566
+ }
3567
+ function decodeSourceMap(raw) {
3568
+ const sources = [];
3569
+ const lines = [];
3570
+ const add = (map, lineOffset, columnOffset) => {
3571
+ if (map.sections) {
3572
+ for (const section of map.sections) {
3573
+ if (!add(section.map, section.offset.line, section.offset.column)) return false;
3574
+ }
3575
+ return true;
3576
+ }
3577
+ if (typeof map.mappings !== "string" || !Array.isArray(map.sources)) return false;
3578
+ const base = sources.length;
3579
+ for (const s of map.sources) sources.push(joinRoot(map.sourceRoot, s ?? ""));
3580
+ decodeMappings(map.mappings, base).forEach((segments, i) => {
3581
+ const at = lineOffset + i;
3582
+ const shifted = i === 0 && columnOffset ? segments.columns.map((c) => c + columnOffset) : segments.columns;
3583
+ const existing = lines[at];
3584
+ if (!existing) lines[at] = { columns: shifted, sources: segments.sources };
3585
+ else {
3586
+ existing.columns.push(...shifted);
3587
+ existing.sources.push(...segments.sources);
3588
+ }
3589
+ });
3590
+ return true;
3591
+ };
3592
+ if (raw.version !== void 0 && raw.version !== 3) return null;
3593
+ if (!add(raw, 0, 0)) return null;
3594
+ return {
3595
+ sources,
3596
+ sourceAt(line, column) {
3597
+ const segments = lines[line];
3598
+ if (!segments || segments.columns.length === 0) return null;
3599
+ let lo = 0;
3600
+ let hi = segments.columns.length - 1;
3601
+ if (segments.columns[0] > column) return null;
3602
+ while (lo < hi) {
3603
+ const mid = lo + hi + 1 >> 1;
3604
+ if (segments.columns[mid] <= column) lo = mid;
3605
+ else hi = mid - 1;
3606
+ }
3607
+ const source = segments.sources[lo];
3608
+ return source < 0 ? null : source;
3609
+ },
3610
+ mapsBetween(source, start, end) {
3611
+ for (let line = start.line; line <= end.line && line < lines.length; line++) {
3612
+ const segments = lines[line];
3613
+ if (!segments) continue;
3614
+ let i = 0;
3615
+ if (line === start.line) {
3616
+ let hi = segments.columns.length;
3617
+ while (i < hi) {
3618
+ const mid = i + hi >> 1;
3619
+ if (segments.columns[mid] < start.column) i = mid + 1;
3620
+ else hi = mid;
3621
+ }
3622
+ }
3623
+ for (; i < segments.columns.length; i++) {
3624
+ if (line === end.line && segments.columns[i] >= end.column) return false;
3625
+ if (segments.sources[i] === source) return true;
3626
+ }
3627
+ }
3628
+ return false;
3629
+ }
3630
+ };
3631
+ }
3632
+ function lineStarts(text) {
3633
+ const starts = [0];
3634
+ for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
3635
+ return starts;
3636
+ }
3637
+ function offsetToPosition(starts, offset) {
3638
+ let lo = 0;
3639
+ let hi = starts.length - 1;
3640
+ while (lo < hi) {
3641
+ const mid = lo + hi + 1 >> 1;
3642
+ if (starts[mid] <= offset) lo = mid;
3643
+ else hi = mid - 1;
3644
+ }
3645
+ return { line: lo, column: offset - starts[lo] };
3646
+ }
3647
+ function sourceMappingUrl(script) {
3648
+ const tail = script.slice(-4096);
3649
+ const matches = [...tail.matchAll(/(?:\/\/|\/\*)[#@]\s*sourceMappingURL=([^\s*'"]+)/g)];
3650
+ return matches.length ? matches[matches.length - 1][1] : null;
3651
+ }
3652
+ function decodeDataUrl(url) {
3653
+ const m = /^data:([^,]*?)(;base64)?,(.*)$/s.exec(url);
3654
+ if (!m) return null;
3655
+ try {
3656
+ if (!m[2]) return decodeURIComponent(m[3]);
3657
+ const binary = globalThis.atob(m[3]);
3658
+ const bytes = new Uint8Array(binary.length);
3659
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
3660
+ return new TextDecoder().decode(bytes);
3661
+ } catch {
3662
+ return null;
3663
+ }
3664
+ }
3665
+ function normalizeSourcePath(source) {
3666
+ let s = source.replace(/[?#].*$/, "");
3667
+ s = s.replace(/^webpack:\/\/[^/]*\//, "").replace(/^webpack:\/\//, "");
3668
+ s = s.replace(/^(?:vite|rollup|turbopack|ng):\/*/, "");
3669
+ s = s.replace(/^\[project\]\//, "");
3670
+ s = s.replace(/^file:\/\//, "");
3671
+ s = s.replace(/^\/@fs\//, "/");
3672
+ s = s.replace(/^\/@id\//, "");
3673
+ try {
3674
+ s = decodeURIComponent(s);
3675
+ } catch {
3676
+ }
3677
+ s = s.replace(/\\/g, "/");
3678
+ if (/^\/[A-Za-z]:\//.test(s)) s = s.slice(1);
3679
+ s = s.replace(/(^|\/)\.\//g, "$1");
3680
+ return s;
3681
+ }
3682
+
3683
+ // ../core/src/code-reach.ts
3684
+ var MAX_CODE_REACH_FILES = 2e3;
3685
+ function executedFunctionRanges(entry) {
3686
+ const length = entry.source?.length ?? Number.POSITIVE_INFINITY;
3687
+ const out = [];
3688
+ entry.functions.forEach((fn, i) => {
3689
+ const whole = fn.ranges[0];
3690
+ if (!whole || whole.count <= 0) return;
3691
+ const topLevel = whole.startOffset === 0 && (whole.endOffset >= length || i === 0 && fn.functionName === "");
3692
+ if (topLevel) return;
3693
+ out.push({ start: whole.startOffset, end: whole.endOffset });
3694
+ });
3695
+ return out;
3696
+ }
3697
+ function executedFunctionOffsets(entry) {
3698
+ return executedFunctionRanges(entry).map((range) => range.start);
3699
+ }
3700
+ function scriptRan(entry) {
3701
+ return executedFunctionOffsets(entry).length > 0;
3702
+ }
3703
+ function reachedSources(entry, map) {
3704
+ const ranges = executedFunctionRanges(entry);
3705
+ const reached = /* @__PURE__ */ new Set();
3706
+ if (!ranges.length || entry.source === void 0) return reached;
3707
+ const starts = lineStarts(entry.source);
3708
+ for (const range of ranges) {
3709
+ const start = offsetToPosition(starts, range.start);
3710
+ const source = map.sourceAt(start.line, start.column);
3711
+ if (source === null || reached.has(source)) continue;
3712
+ if (map.mapsBetween(source, start, offsetToPosition(starts, range.end))) reached.add(source);
3713
+ }
3714
+ return reached;
3715
+ }
3716
+ function finalizeCodeReach(files) {
3717
+ const out = /* @__PURE__ */ new Set();
3718
+ for (const raw of files) {
3719
+ const file = raw.replace(/\\/g, "/").replace(/^\.\//, "");
3720
+ if (!file || file.startsWith("../") || file.startsWith("/") || /^[A-Za-z]:\//.test(file)) continue;
3721
+ if (/(^|\/)node_modules\//.test(file)) continue;
3722
+ out.add(file);
3723
+ }
3724
+ return [...out].sort().slice(0, MAX_CODE_REACH_FILES);
3725
+ }
3726
+
3727
+ // src/internal/capture/code-reach.ts
3728
+ var MAX_MAP_BYTES = 20 * 1024 * 1024;
3729
+ var MAP_TIMEOUT_MS = 5e3;
3730
+ var SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|marko|html?)$/i;
3731
+ var REPLAYED_SCRIPT = /^(?:about|rsc):/i;
3732
+ var BUILD_DIRS = /(?:^|\/)(?:dist|build|out|\.output|\.next|\.nuxt|\.svelte-kit|public|static|wwwroot)\//;
3733
+ var MAX_CACHED_SCRIPTS = 500;
3734
+ var LatestByUrl = class {
3735
+ constructor() {
3736
+ this.entries = /* @__PURE__ */ new Map();
3737
+ }
3738
+ get(url, version) {
3739
+ const entry = this.entries.get(url);
3740
+ if (!entry || entry.version !== version) return void 0;
3741
+ this.entries.delete(url);
3742
+ this.entries.set(url, entry);
3743
+ return entry.value;
3744
+ }
3745
+ set(url, version, value) {
3746
+ this.entries.delete(url);
3747
+ this.entries.set(url, { version, value });
3748
+ if (this.entries.size > MAX_CACHED_SCRIPTS) this.entries.delete(this.entries.keys().next().value);
3749
+ }
3750
+ clear() {
3751
+ this.entries.clear();
3752
+ }
3753
+ };
3754
+ var scriptCache = new LatestByUrl();
3755
+ var mapCache = new LatestByUrl();
3756
+ var cachedRepoRoot = null;
3757
+ function repositoryRoot(from) {
3758
+ if (cachedRepoRoot?.from === from) return cachedRepoRoot.root;
3759
+ let root = from;
3760
+ try {
3761
+ root = (0, import_node_child_process.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
3762
+ cwd: from,
3763
+ encoding: "utf-8",
3764
+ stdio: ["ignore", "pipe", "ignore"]
3765
+ }).trim();
3766
+ } catch {
3767
+ }
3768
+ cachedRepoRoot = { from, root: path9.resolve(root) };
3769
+ return cachedRepoRoot.root;
3770
+ }
3771
+ function codeReachRoots(configDir, configured) {
3772
+ const repoRoot = repositoryRoot(configDir);
3773
+ const roots = configured?.length ? configured.map((r) => path9.resolve(configDir, r)) : [configDir];
3774
+ return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
3775
+ }
3776
+ function isFile(file) {
3777
+ try {
3778
+ return fs6.statSync(file).isFile();
3779
+ } catch {
3780
+ return false;
3781
+ }
3782
+ }
3783
+ function resolveSourcePath(source, roots) {
3784
+ const normalized = normalizeSourcePath(source);
3785
+ if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
3786
+ if (path9.isAbsolute(normalized) && isFile(normalized)) return path9.resolve(normalized);
3787
+ const relative7 = normalized.replace(/^\/+/, "");
3788
+ for (const root of roots) {
3789
+ const candidate = path9.resolve(root, relative7);
3790
+ if (isFile(candidate)) return candidate;
3791
+ }
3792
+ return null;
3793
+ }
3794
+ function viteBases(entries) {
3795
+ const bases = /* @__PURE__ */ new Map();
3796
+ for (const entry of entries) {
3797
+ const match = /^(https?:\/\/[^/]+)(\/.*?)@vite\/client$/.exec(entry.url.replace(/[?#].*$/, ""));
3798
+ if (match && match[2] !== "/") bases.set(match[1], match[2]);
3799
+ }
3800
+ return bases;
3801
+ }
3802
+ function fileForUrl(url, roots, bases) {
3803
+ let pathname;
3804
+ try {
3805
+ const parsed = new URL(url);
3806
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" && parsed.protocol !== "file:") return null;
3807
+ pathname = parsed.pathname;
3808
+ const base = bases.get(parsed.origin);
3809
+ if (base && pathname.startsWith(base)) pathname = pathname.slice(base.length - 1);
3810
+ } catch {
3811
+ return null;
3812
+ }
3813
+ if (!SOURCE_EXTENSIONS.test(pathname) || pathname.startsWith("/@vite/") || pathname.includes("/node_modules/")) {
3814
+ return null;
3815
+ }
3816
+ const file = resolveSourcePath(pathname, roots);
3817
+ if (!file || BUILD_DIRS.test(file.replace(/\\/g, "/"))) return null;
3818
+ return file;
3819
+ }
3820
+ function relativeToRepo(file, repoRoot) {
3821
+ return path9.relative(repoRoot, file).split(path9.sep).join("/");
3822
+ }
3823
+ function scriptHash(entry) {
3824
+ return crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
3825
+ }
3826
+ async function loadMap(entry, fetchMap) {
3827
+ const ref = entry.source ? sourceMappingUrl(entry.source) : null;
3828
+ if (!ref) return null;
3829
+ if (ref.startsWith("data:")) {
3830
+ const text = decodeDataUrl(ref);
3831
+ return text ? parseMap(text) : null;
3832
+ }
3833
+ let absolute;
3834
+ try {
3835
+ absolute = new URL(ref, entry.url).href;
3836
+ } catch {
3837
+ return null;
3838
+ }
3839
+ const version = scriptHash(entry);
3840
+ let pending = mapCache.get(absolute, version);
3841
+ if (!pending) {
3842
+ pending = fetchMap(absolute).then((text) => text ? parseMap(text) : null);
3843
+ mapCache.set(absolute, version, pending);
3844
+ }
3845
+ return pending;
3846
+ }
3847
+ function parseMap(text) {
3848
+ try {
3849
+ return decodeSourceMap(JSON.parse(text));
3850
+ } catch {
3851
+ return null;
3852
+ }
3853
+ }
3854
+ async function resolveScript(entry, roots, bases, fetchMap) {
3855
+ const version = scriptHash(entry);
3856
+ const cached = scriptCache.get(entry.url, version);
3857
+ if (cached) return cached;
3858
+ let resolution;
3859
+ const file = fileForUrl(entry.url, roots.roots, bases);
3860
+ if (file) resolution = { kind: "file", file, lookup: await loadMap(entry, fetchMap) };
3861
+ else {
3862
+ const lookup = await loadMap(entry, fetchMap);
3863
+ resolution = lookup ? { kind: "map", lookup, files: [] } : { kind: "none" };
3864
+ }
3865
+ scriptCache.set(entry.url, version, resolution);
3866
+ return resolution;
3867
+ }
3868
+ async function resolveCodeReach(entries, roots, fetchMap) {
3869
+ const files = /* @__PURE__ */ new Set();
3870
+ const bases = viteBases(entries);
3871
+ for (const entry of entries) {
3872
+ if (!entry.url || !entry.functions?.length || REPLAYED_SCRIPT.test(entry.url)) continue;
3873
+ const resolution = await resolveScript(entry, roots, bases, fetchMap);
3874
+ if (resolution.kind === "file") {
3875
+ const ran = resolution.lookup ? reachedSources(entry, resolution.lookup).size > 0 : scriptRan(entry);
3876
+ if (resolution.file && ran) files.add(relativeToRepo(resolution.file, roots.repoRoot));
3877
+ } else if (resolution.kind === "map") {
3878
+ for (const index of reachedSources(entry, resolution.lookup)) {
3879
+ if (resolution.files[index] === void 0) {
3880
+ const source = resolution.lookup.sources[index] ?? "";
3881
+ let absolute = resolveSourcePath(source, roots.roots);
3882
+ if (!absolute && !/^[a-z][\w+.-]*:/i.test(source)) {
3883
+ try {
3884
+ absolute = fileForUrl(new URL(source, entry.url).href, roots.roots, bases);
3885
+ } catch {
3886
+ absolute = null;
3887
+ }
3888
+ }
3889
+ resolution.files[index] = absolute;
3890
+ }
3891
+ const file = resolution.files[index];
3892
+ if (file) files.add(relativeToRepo(file, roots.repoRoot));
3893
+ }
3894
+ }
3895
+ }
3896
+ return finalizeCodeReach(files);
3897
+ }
3898
+ function pageMapFetcher(page) {
3899
+ return async (url) => {
3900
+ try {
3901
+ const response = await internalCall(page, () => page.request.get(url, { timeout: MAP_TIMEOUT_MS }));
3902
+ if (!response.ok()) return null;
3903
+ const length = Number(response.headers()["content-length"] ?? 0);
3904
+ if (length > MAX_MAP_BYTES) return null;
3905
+ const body = await response.body();
3906
+ return body.length > MAX_MAP_BYTES ? null : body.toString("utf-8");
3907
+ } catch {
3908
+ return null;
3909
+ }
3910
+ };
3911
+ }
3912
+ function isChromiumPage(page) {
3913
+ try {
3914
+ return page.context().browser()?.browserType().name() === "chromium";
3915
+ } catch {
3916
+ return false;
3917
+ }
3918
+ }
3919
+ async function startCodeReach(page) {
3920
+ if (!isChromiumPage(page) || !page.coverage) return false;
3921
+ try {
3922
+ await internalCall(
3923
+ page,
3924
+ () => page.coverage.startJSCoverage({ resetOnNavigation: false, reportAnonymousScripts: false })
3925
+ );
3926
+ return true;
3927
+ } catch {
3928
+ return false;
3929
+ }
3930
+ }
3931
+ async function stopCodeReach(page) {
3932
+ try {
3933
+ return await internalCall(page, () => page.coverage.stopJSCoverage());
3934
+ } catch {
3935
+ return null;
3936
+ }
3937
+ }
3938
+
2990
3939
  // src/internal/capture/capture-fixtures.ts
2991
3940
  function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
2992
3941
  const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
@@ -3036,11 +3985,53 @@ function createSink() {
3036
3985
  pageInventories: [],
3037
3986
  probeItem: null,
3038
3987
  probeInterception: null,
3988
+ flake: null,
3039
3989
  pickOffered: false,
3040
3990
  userPick: null,
3041
- locatorPages: new LocatorPageLog()
3991
+ locatorPages: new LocatorPageLog(),
3992
+ codeReach: null
3042
3993
  };
3043
3994
  }
3995
+ function maybeStartCodeReach(page) {
3996
+ const sink = currentSink;
3997
+ if (!sink || process.env.PIWI_CAPTURE_CODE_REACH !== "true") return Promise.resolve(false);
3998
+ if (sink.codeReach) return sink.codeReach.page === page ? sink.codeReach.started : Promise.resolve(false);
3999
+ const started = startCodeReach(page);
4000
+ sink.codeReach = { page, started, stopped: false, files: null };
4001
+ return started;
4002
+ }
4003
+ function configuredCodeReachRoots() {
4004
+ const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
4005
+ if (!raw) return null;
4006
+ try {
4007
+ const parsed = JSON.parse(raw);
4008
+ if (Array.isArray(parsed)) return parsed.filter((r) => typeof r === "string" && r !== "");
4009
+ } catch {
4010
+ }
4011
+ return raw.split(path10.delimiter).filter(Boolean);
4012
+ }
4013
+ async function stopSinkCodeReach(sink, closing) {
4014
+ const session = sink.codeReach;
4015
+ if (!session || session.stopped) return;
4016
+ if (closing && closing.page !== session.page && (!closing.context || pageContext(session.page) !== closing.context)) {
4017
+ return;
4018
+ }
4019
+ session.stopped = true;
4020
+ try {
4021
+ if (!await session.started || isPageClosed(session.page)) return;
4022
+ const entries = await stopCodeReach(session.page);
4023
+ if (!entries) return;
4024
+ const info = sink.testInfo;
4025
+ const configFile = info?.config.configFile;
4026
+ const configDir = configFile ? path10.dirname(configFile) : info?.config.rootDir ?? process.cwd();
4027
+ session.files = await resolveCodeReach(
4028
+ entries,
4029
+ codeReachRoots(configDir, configuredCodeReachRoots()),
4030
+ pageMapFetcher(session.page)
4031
+ );
4032
+ } catch {
4033
+ }
4034
+ }
3044
4035
  var currentSink = null;
3045
4036
  var PENDING_PROBES = /* @__PURE__ */ new Set();
3046
4037
  async function drainPendingProbes(capMs) {
@@ -3048,8 +4039,8 @@ async function drainPendingProbes(capMs) {
3048
4039
  let cap;
3049
4040
  await Promise.race([
3050
4041
  Promise.allSettled(PENDING_PROBES),
3051
- new Promise((resolve2) => {
3052
- cap = setTimeout(resolve2, capMs);
4042
+ new Promise((resolve3) => {
4043
+ cap = setTimeout(resolve3, capMs);
3053
4044
  })
3054
4045
  ]);
3055
4046
  clearTimeout(cap);
@@ -3089,10 +4080,10 @@ async function readWebVitals(page) {
3089
4080
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
3090
4081
  paint[key] = Math.round(entry.startTime);
3091
4082
  }
3092
- const readBuffered = (type, extra) => new Promise((resolve2) => {
4083
+ const readBuffered = (type, extra) => new Promise((resolve3) => {
3093
4084
  try {
3094
4085
  const PO = globalThis.PerformanceObserver;
3095
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve2(null);
4086
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve3(null);
3096
4087
  const out = [];
3097
4088
  const po = new PO((list) => out.push(...list.getEntries()));
3098
4089
  po.observe({ type, buffered: true, ...extra });
@@ -3102,10 +4093,10 @@ async function readWebVitals(page) {
3102
4093
  po.disconnect();
3103
4094
  } catch {
3104
4095
  }
3105
- resolve2(out);
4096
+ resolve3(out);
3106
4097
  }, 0);
3107
4098
  } catch {
3108
- resolve2(null);
4099
+ resolve3(null);
3109
4100
  }
3110
4101
  });
3111
4102
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -3218,12 +4209,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
3218
4209
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
3219
4210
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
3220
4211
  function withTimeout(promise, ms) {
3221
- return new Promise((resolve2, reject) => {
4212
+ return new Promise((resolve3, reject) => {
3222
4213
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
3223
4214
  promise.then(
3224
4215
  (value) => {
3225
4216
  clearTimeout(timer);
3226
- resolve2(value);
4217
+ resolve3(value);
3227
4218
  },
3228
4219
  (error) => {
3229
4220
  clearTimeout(timer);
@@ -3550,9 +4541,11 @@ function wrapFrameLocator(page, frameLocator) {
3550
4541
  }
3551
4542
  });
3552
4543
  }
4544
+ var CANCELLED_REQUEST = /^net::ERR_ABORTED$|^NS_BINDING_ABORTED$|^NS_ERROR_ABORT$|cancell?ed/i;
3553
4545
  function instrumentPage(page) {
3554
4546
  if (!page || INSTRUMENTED_PAGES.has(page)) return;
3555
4547
  INSTRUMENTED_PAGES.add(page);
4548
+ void maybeStartCodeReach(page);
3556
4549
  const probeSink = currentSink;
3557
4550
  if (probeSink?.probeItem && !probeSink.probeInterception) {
3558
4551
  void installProbeInterception(page, probeSink.probeItem).then((interception) => {
@@ -3560,6 +4553,18 @@ function instrumentPage(page) {
3560
4553
  }).catch(() => {
3561
4554
  });
3562
4555
  }
4556
+ const flake = currentSink?.flake;
4557
+ if (flake?.role === "target" && !flake.conditions) {
4558
+ flake.browserName = pageBrowserName(page) ?? flake.browserName;
4559
+ const plan = flake.plan;
4560
+ flake.conditions = installFlakeConditions(page, plan).catch((error) => {
4561
+ const note = `conditions failed to install: ${error instanceof Error ? error.message : String(error)}`;
4562
+ const reports = unappliedReports(plan).map(
4563
+ (r) => r.outcome === "by-command" ? r : { ...r, outcome: "skipped", note }
4564
+ );
4565
+ return { reports: () => reports };
4566
+ });
4567
+ }
3563
4568
  const ctx = pageContext(page);
3564
4569
  if (ctx) instrumentContext(ctx);
3565
4570
  if (typeof page.close === "function") {
@@ -3570,6 +4575,7 @@ function instrumentPage(page) {
3570
4575
  if (sink) {
3571
4576
  await stashPageState(sink, { page });
3572
4577
  await maybeOpenPicker(sink, { page });
4578
+ await stopSinkCodeReach(sink, { page });
3573
4579
  }
3574
4580
  return originalClose(...args);
3575
4581
  };
@@ -3641,7 +4647,7 @@ function instrumentPage(page) {
3641
4647
  } catch {
3642
4648
  }
3643
4649
  }
3644
- page.on("requestfinished", (request) => {
4650
+ const recordRequest = (request, failed) => {
3645
4651
  const sink = currentSink;
3646
4652
  if (!sink) return;
3647
4653
  sink.lastActivePage = page;
@@ -3649,18 +4655,21 @@ function instrumentPage(page) {
3649
4655
  try {
3650
4656
  const url = request.url();
3651
4657
  if (url.startsWith("data:") || url.startsWith("blob:")) return;
3652
- const timing = request.timing();
3653
- const response = await request.response();
3654
4658
  const resourceType = request.resourceType();
3655
- if (!["fetch", "xhr", "document", "other"].includes(resourceType)) return;
4659
+ if (!TRACKED_REQUEST_TYPES.includes(resourceType)) return;
4660
+ const failure = failed ? request.failure()?.errorText ?? "failed" : null;
4661
+ if (failure && CANCELLED_REQUEST.test(failure)) return;
4662
+ const timing = request.timing();
4663
+ const response = failed ? null : await request.response();
3656
4664
  const entry = {
3657
4665
  method: request.method(),
3658
4666
  url,
3659
4667
  status: response ? response.status() : 0,
3660
- duration: timing.responseEnd > 0 ? Math.round(timing.responseEnd - timing.requestStart) : 0,
4668
+ duration: requestDuration(timing, failed),
3661
4669
  startTime: timing.startTime,
3662
4670
  resourceType
3663
4671
  };
4672
+ if (failure) entry.failure = failure;
3664
4673
  if (response) {
3665
4674
  const headers = response.headers();
3666
4675
  const contentType = headers["content-type"];
@@ -3685,7 +4694,12 @@ function instrumentPage(page) {
3685
4694
  }
3686
4695
  })();
3687
4696
  sink.pendingHandlers.push(p);
3688
- });
4697
+ };
4698
+ page.on("requestfinished", (request) => recordRequest(request, false));
4699
+ page.on("requestfailed", (request) => recordRequest(request, true));
4700
+ }
4701
+ async function awaitFlakeConditions() {
4702
+ await currentSink?.flake?.conditions;
3689
4703
  }
3690
4704
  function instrumentContext(context) {
3691
4705
  if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
@@ -3698,6 +4712,8 @@ function instrumentContext(context) {
3698
4712
  context.newPage = async (...args) => {
3699
4713
  const page = await originalNewPage(...args);
3700
4714
  instrumentPage(page);
4715
+ await awaitFlakeConditions();
4716
+ await maybeStartCodeReach(page);
3701
4717
  return page;
3702
4718
  };
3703
4719
  if (typeof context.close === "function") {
@@ -3708,6 +4724,7 @@ function instrumentContext(context) {
3708
4724
  if (sink) {
3709
4725
  await stashPageState(sink, { context });
3710
4726
  await maybeOpenPicker(sink, { context });
4727
+ await stopSinkCodeReach(sink, { context });
3711
4728
  }
3712
4729
  return originalClose(...args);
3713
4730
  };
@@ -3721,6 +4738,8 @@ function patchBrowser(browser) {
3721
4738
  browser.newPage = async (...args) => {
3722
4739
  const page = await originalNewPage(...args);
3723
4740
  instrumentPage(page);
4741
+ await awaitFlakeConditions();
4742
+ await maybeStartCodeReach(page);
3724
4743
  return page;
3725
4744
  };
3726
4745
  const originalNewContext = browser.newContext.bind(browser);
@@ -3743,13 +4762,19 @@ function readRootAria(page) {
3743
4762
  function readRootAriaJson(page) {
3744
4763
  return internalCall(page, () => ariaSnapshotJSONBestEffort(page.locator(":root")));
3745
4764
  }
4765
+ var TRACKED_REQUEST_TYPES = ["fetch", "xhr", "document", "other"];
4766
+ function requestDuration(timing, failed) {
4767
+ if (timing.responseEnd > 0) return Math.round(timing.responseEnd - timing.requestStart);
4768
+ if (failed && timing.startTime > 0) return Math.max(0, Math.round(Date.now() - timing.startTime));
4769
+ return 0;
4770
+ }
3746
4771
  async function flushSink(sink, testInfo) {
3747
4772
  await Promise.allSettled(sink.pendingHandlers);
3748
4773
  let drainDeadline;
3749
4774
  await Promise.race([
3750
4775
  Promise.allSettled(sink.capturePromises),
3751
- new Promise((resolve2) => {
3752
- drainDeadline = setTimeout(resolve2, 2e3);
4776
+ new Promise((resolve3) => {
4777
+ drainDeadline = setTimeout(resolve3, 2e3);
3753
4778
  })
3754
4779
  ]);
3755
4780
  clearTimeout(drainDeadline);
@@ -3765,6 +4790,13 @@ async function flushSink(sink, testInfo) {
3765
4790
  body: Buffer.from(JSON.stringify(dedupeSnapshotsByLocation(sink.capturedLocators)))
3766
4791
  });
3767
4792
  }
4793
+ await stopSinkCodeReach(sink);
4794
+ if (sink.codeReach?.files?.length) {
4795
+ await testInfo.attach(ATTACHMENT_NAMES.codeReach, {
4796
+ contentType: "application/json",
4797
+ body: Buffer.from(JSON.stringify(sink.codeReach.files))
4798
+ });
4799
+ }
3768
4800
  if (sink.locatorPages.size > 0) {
3769
4801
  await testInfo.attach(ATTACHMENT_NAMES.locatorPages, {
3770
4802
  contentType: "application/json",
@@ -3898,6 +4930,8 @@ var piwiFixtures = {
3898
4930
  // never double-wraps a page the browser patch already instrumented.
3899
4931
  page: async ({ page }, use) => {
3900
4932
  instrumentPage(page);
4933
+ await awaitFlakeConditions();
4934
+ await maybeStartCodeReach(page);
3901
4935
  await use(page);
3902
4936
  },
3903
4937
  // Auto, test-scoped: open a capture sink for the running test and flush it
@@ -3906,8 +4940,23 @@ var piwiFixtures = {
3906
4940
  piwiCapture: [
3907
4941
  async ({}, use, testInfo) => {
3908
4942
  boxCaptureFrames();
4943
+ const conflict = labModeConflict();
4944
+ if (conflict) throw new Error(conflict);
3909
4945
  const sink = createSink();
3910
4946
  sink.testInfo = testInfo;
4947
+ if (isFlakeMode()) {
4948
+ const plan = loadFlakePlan();
4949
+ const role = flakeRoleForTest(plan, {
4950
+ file: testInfo.file,
4951
+ title: testInfo.title,
4952
+ titlePath: testInfo.titlePath,
4953
+ project: testInfo.project.name
4954
+ });
4955
+ if (role) {
4956
+ const browserName = testInfo.project.use?.browserName ?? null;
4957
+ sink.flake = { plan, role, startedAt: Date.now(), browserName, conditions: null };
4958
+ }
4959
+ }
3911
4960
  if (isProbeMode())
3912
4961
  sink.probeItem = probeItemForTest({
3913
4962
  title: testInfo.title,
@@ -3938,12 +4987,34 @@ var piwiFixtures = {
3938
4987
  handled
3939
4988
  });
3940
4989
  }
4990
+ if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
3941
4991
  await flushSink(sink, testInfo);
3942
4992
  }
3943
4993
  },
3944
4994
  { auto: true }
3945
4995
  ]
3946
4996
  };
4997
+ async function recordFlakeAttempt(flake, testInfo) {
4998
+ const installed = flake.conditions ? await flake.conditions : null;
4999
+ recordFlakeResult(
5000
+ flakeResultLine(flake.plan, {
5001
+ role: flake.role,
5002
+ file: testInfo.file,
5003
+ title: testInfo.title,
5004
+ project: testInfo.project.name || null,
5005
+ browserName: flake.browserName,
5006
+ status: testInfo.status ?? "passed",
5007
+ errorText: joinErrorMessages(testInfo.errors),
5008
+ startedAt: flake.startedAt,
5009
+ duration: Date.now() - flake.startedAt,
5010
+ workerIndex: testInfo.workerIndex,
5011
+ parallelIndex: testInfo.parallelIndex,
5012
+ repeatEachIndex: testInfo.repeatEachIndex,
5013
+ retry: testInfo.retry,
5014
+ conditions: installed ? installed.reports() : unappliedReports(flake.plan)
5015
+ })
5016
+ );
5017
+ }
3947
5018
  function extendPiwiFixtures(test) {
3948
5019
  return test.extend(piwiFixtures);
3949
5020
  }
@@ -3956,11 +5027,17 @@ function extendPiwiFixtures(test) {
3956
5027
  boxCaptureFrames,
3957
5028
  buildPageState,
3958
5029
  callMethod,
5030
+ codeReachRoots,
3959
5031
  computeCoreVitals,
5032
+ configuredCodeReachRoots,
3960
5033
  extendPiwiFixtures,
3961
5034
  internalCall,
3962
5035
  noteLocatorCall,
3963
5036
  noteNavigation,
5037
+ pageMapFetcher,
3964
5038
  piwiFixtures,
3965
- probeElementAttrs
5039
+ probeElementAttrs,
5040
+ resolveCodeReach,
5041
+ startCodeReach,
5042
+ stopCodeReach
3966
5043
  });