@piwitests/reporter 0.35.0 → 0.38.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.
@@ -40,7 +40,7 @@ __export(capture_fixtures_exports, {
40
40
  probeElementAttrs: () => probeElementAttrs
41
41
  });
42
42
  module.exports = __toCommonJS(capture_fixtures_exports);
43
- var import_node_zlib = require("zlib");
43
+ var import_node_zlib2 = require("zlib");
44
44
 
45
45
  // ../picker-dom/src/probe.ts
46
46
  function probeElementAttrs(el, arg) {
@@ -1742,6 +1742,7 @@ var ATTACHMENT_NAMES = {
1742
1742
  console: "piwi-console",
1743
1743
  dialogs: "piwi-dialogs",
1744
1744
  network: "piwi-network",
1745
+ pageInventory: "piwi-page-inventory",
1745
1746
  webVitals: "piwi-web-vitals",
1746
1747
  locatorSuggestion: "piwi-locator-suggestion",
1747
1748
  pageState: "piwi-page-state",
@@ -1753,6 +1754,512 @@ var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
1753
1754
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
1754
1755
  var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
1755
1756
 
1757
+ // src/internal/capture/page-inventory.ts
1758
+ var PAGE_INVENTORY_MAX_ENTRIES = 500;
1759
+ function collectPageInventoryInPage(maxEntries) {
1760
+ const g = globalThis;
1761
+ const doc = g.document;
1762
+ if (!doc || !g.location) return null;
1763
+ const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
1764
+ const roleOf = (el) => {
1765
+ const explicit = el.getAttribute("role");
1766
+ if (explicit) return explicit.toLowerCase();
1767
+ const tag = (el.tagName || "").toLowerCase();
1768
+ if (tag === "a") return el.getAttribute("href") != null ? "link" : "generic";
1769
+ if (tag === "button") return "button";
1770
+ if (tag === "select") return el.hasAttribute("multiple") ? "listbox" : "combobox";
1771
+ if (tag === "textarea") return "textbox";
1772
+ if (tag === "input") {
1773
+ const type = (el.getAttribute("type") || "text").toLowerCase();
1774
+ const map = {
1775
+ checkbox: "checkbox",
1776
+ radio: "radio",
1777
+ button: "button",
1778
+ submit: "button",
1779
+ reset: "button",
1780
+ image: "button",
1781
+ range: "slider",
1782
+ search: "searchbox",
1783
+ email: "textbox",
1784
+ tel: "textbox",
1785
+ url: "textbox",
1786
+ number: "spinbutton"
1787
+ };
1788
+ return map[type] || "textbox";
1789
+ }
1790
+ return "generic";
1791
+ };
1792
+ const holdsValue = (el, role) => {
1793
+ const tag = (el.tagName || "").toLowerCase();
1794
+ if (tag === "select" || tag === "textarea" || tag === "input") return true;
1795
+ if (el.isContentEditable === true) return true;
1796
+ const editable = el.getAttribute("contenteditable");
1797
+ if (editable === "" || editable === "true") return true;
1798
+ return role === "textbox" || role === "combobox" || role === "listbox" || role === "searchbox" || role === "spinbutton";
1799
+ };
1800
+ const labelText = (el) => {
1801
+ const labels = el.labels;
1802
+ if (!labels || labels.length === 0) return "";
1803
+ let text = "";
1804
+ for (const label of labels) text += ` ${label.textContent || ""}`;
1805
+ return text;
1806
+ };
1807
+ const labelledByText = (el) => {
1808
+ const ids = (el.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
1809
+ if (ids.length === 0) return "";
1810
+ let text = "";
1811
+ for (const id of ids) {
1812
+ const ref = doc.getElementById(id);
1813
+ if (ref) text += ` ${ref.textContent || ""}`;
1814
+ }
1815
+ return text;
1816
+ };
1817
+ const nameOf = (el) => {
1818
+ const role = roleOf(el);
1819
+ const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
1820
+ if (!holdsValue(el, role)) chain.push(el.textContent);
1821
+ chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
1822
+ return clean(chain.find((s) => s && String(s).trim()) || "");
1823
+ };
1824
+ const cleanHref = (href) => {
1825
+ const cut = href.search(/[?#]/);
1826
+ return cut >= 0 ? href.slice(0, cut) : href;
1827
+ };
1828
+ const controls = [];
1829
+ const links = [];
1830
+ const seenC = /* @__PURE__ */ new Set();
1831
+ const seenL = /* @__PURE__ */ new Set();
1832
+ let budget = maxEntries;
1833
+ const controlSelector = 'button, [role="button"], input, select, textarea, [role="checkbox"], [role="radio"], [role="tab"], [role="menuitem"], [role="combobox"], [role="switch"], [role="slider"], [role="searchbox"], [role="spinbutton"], [role="textbox"], [role="listbox"]';
1834
+ for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
1835
+ if (budget <= 0) break;
1836
+ const role = roleOf(el);
1837
+ const name = nameOf(el);
1838
+ if (!name) continue;
1839
+ const key = `${role}\0${name}`;
1840
+ if (seenC.has(key)) continue;
1841
+ seenC.add(key);
1842
+ controls.push({ role, name });
1843
+ budget--;
1844
+ }
1845
+ for (const el of Array.from(doc.querySelectorAll("a[href]"))) {
1846
+ if (budget <= 0) break;
1847
+ const name = nameOf(el);
1848
+ const href = cleanHref(el.getAttribute("href") || "");
1849
+ if (!name) continue;
1850
+ const key = `${name}\0${href}`;
1851
+ if (seenL.has(key)) continue;
1852
+ seenL.add(key);
1853
+ links.push({ name, href });
1854
+ budget--;
1855
+ }
1856
+ return { url: g.location.href, controls, links };
1857
+ }
1858
+ function capPageInventory(inventory, max = PAGE_INVENTORY_MAX_ENTRIES) {
1859
+ const controls = inventory.controls.slice(0, Math.max(0, max));
1860
+ const links = inventory.links.slice(0, Math.max(0, max - controls.length));
1861
+ return { url: inventory.url, controls, links, capturedAt: inventory.capturedAt };
1862
+ }
1863
+ function inventoryPageKey(url) {
1864
+ try {
1865
+ const u = new URL(url);
1866
+ return `${u.origin}${u.pathname}`;
1867
+ } catch {
1868
+ return null;
1869
+ }
1870
+ }
1871
+
1872
+ // src/internal/probe/mode.ts
1873
+ var fs2 = __toESM(require("fs"));
1874
+ var path3 = __toESM(require("path"));
1875
+
1876
+ // src/internal/config/desktop.ts
1877
+ var fs = __toESM(require("fs"));
1878
+ var os = __toESM(require("os"));
1879
+ var path2 = __toESM(require("path"));
1880
+
1881
+ // src/internal/config/env.ts
1882
+ var DEFAULTS = {
1883
+ projectName: "default-project",
1884
+ uploadTraces: true,
1885
+ uploadReport: true,
1886
+ liveFileUploads: true,
1887
+ collectScmInfo: true,
1888
+ collectCiInfo: true,
1889
+ collectPerformanceMetrics: true,
1890
+ captureLocators: true,
1891
+ capturePageState: true,
1892
+ // Off by default: the page inventory reads control and link names from every
1893
+ // visited page, so it stays opt-in until a project turns it on.
1894
+ capturePageInventory: false,
1895
+ captureServerTraces: true,
1896
+ sampleAriaOnPass: true,
1897
+ uploadManifest: true,
1898
+ defaultCapture: true,
1899
+ streaming: true,
1900
+ streamingBatchSize: 5,
1901
+ streamingBatchDelay: 2e3,
1902
+ maxStreamBufferBytes: 100 * 1024 * 1024,
1903
+ failOnFlakyTests: false,
1904
+ username: null,
1905
+ password: null,
1906
+ apiKey: null,
1907
+ verbose: false
1908
+ };
1909
+ var PIWI_ENV_KEYS = {
1910
+ serverUrl: "PIWI_DASHBOARD_URL",
1911
+ projectName: "PIWI_PROJECT_NAME",
1912
+ verbose: "PIWI_VERBOSE",
1913
+ apiKey: "PIWI_API_KEY",
1914
+ username: "PIWI_USERNAME",
1915
+ password: "PIWI_PASSWORD",
1916
+ environment: "PIWI_ENVIRONMENT",
1917
+ label: "PIWI_LABEL",
1918
+ runLabel: "PIWI_RUN_LABEL",
1919
+ keep: "PIWI_KEEP",
1920
+ streaming: "PIWI_STREAMING",
1921
+ streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
1922
+ streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
1923
+ maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
1924
+ liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
1925
+ failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
1926
+ uploadTraces: "PIWI_UPLOAD_TRACES",
1927
+ uploadReport: "PIWI_UPLOAD_REPORT",
1928
+ captureLocators: "PIWI_CAPTURE_LOCATORS",
1929
+ capturePageState: "PIWI_CAPTURE_PAGE_STATE",
1930
+ capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
1931
+ captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
1932
+ sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
1933
+ uploadManifest: "PIWI_UPLOAD_MANIFEST",
1934
+ defaultCapture: "PIWI_DEFAULT_CAPTURE",
1935
+ inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
1936
+ pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
1937
+ outputFile: "PIWI_OUTPUT_FILE",
1938
+ aiMode: "PIWI_AI",
1939
+ aiDir: "PIWI_AI_DIR",
1940
+ aiOnMiss: "PIWI_AI_ON_MISS",
1941
+ aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
1942
+ aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
1943
+ aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
1944
+ aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
1945
+ aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
1946
+ };
1947
+ var PIWI_PROBE_ENV = {
1948
+ flag: "PIWI_PROBE",
1949
+ plan: "PIWI_PROBE_PLAN",
1950
+ results: "PIWI_PROBE_RESULTS",
1951
+ secret: "PIWI_PROBE_SECRET"
1952
+ };
1953
+ var ENV_FALLBACK_SPECS = [
1954
+ { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
1955
+ { option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
1956
+ { option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
1957
+ { option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
1958
+ { option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
1959
+ { option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
1960
+ { option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
1961
+ { option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
1962
+ { option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
1963
+ { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
1964
+ { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
1965
+ { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
1966
+ { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
1967
+ { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
1968
+ { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
1969
+ { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
1970
+ { option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
1971
+ { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
1972
+ { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
1973
+ { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
1974
+ { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
1975
+ { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
1976
+ { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
1977
+ { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
1978
+ { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
1979
+ { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
1980
+ { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
1981
+ ];
1982
+
1983
+ // src/internal/probe/plan.ts
1984
+ function parseRouteKey(routeKey) {
1985
+ const space = routeKey.indexOf(" ");
1986
+ if (space < 0) return { method: "", pattern: routeKey };
1987
+ return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
1988
+ }
1989
+ function patternToRegex(pattern) {
1990
+ const path6 = pattern.split("?")[0] ?? pattern;
1991
+ const escaped = path6.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
1992
+ return new RegExp(`^${escaped}$`);
1993
+ }
1994
+ function suitePathEqual(a, b) {
1995
+ if (!a || !b || a.length !== b.length) return false;
1996
+ return a.every((title, i) => title === b[i]);
1997
+ }
1998
+ function matchProbeItem(plan, test) {
1999
+ const byFileTitle = plan.items.filter(
2000
+ (item) => item.filePath && test.filePath && item.filePath === test.filePath && item.testTitle === test.title
2001
+ );
2002
+ if (byFileTitle.length === 1) return byFileTitle[0];
2003
+ if (byFileTitle.length > 1) {
2004
+ return byFileTitle.find((item) => suitePathEqual(item.suitePath, test.suitePath)) ?? byFileTitle[0];
2005
+ }
2006
+ for (const item of plan.items) {
2007
+ if (!item.filePath && item.testTitle && test.title && item.testTitle === test.title) return item;
2008
+ }
2009
+ return null;
2010
+ }
2011
+ function requestMatchesRoute(method, url, routeKey) {
2012
+ const { method: rm, pattern } = parseRouteKey(routeKey);
2013
+ if (rm && rm !== method.toUpperCase()) return false;
2014
+ let path6;
2015
+ try {
2016
+ path6 = new URL(url).pathname;
2017
+ } catch {
2018
+ path6 = url.split("?")[0] ?? url;
2019
+ }
2020
+ return patternToRegex(pattern).test(path6);
2021
+ }
2022
+ function createProbeState() {
2023
+ return { navigated: false, matches: 0 };
2024
+ }
2025
+ function markNavigated(state) {
2026
+ state.navigated = true;
2027
+ }
2028
+ function shouldMutate(state, item) {
2029
+ if (!state.navigated) return false;
2030
+ state.matches += 1;
2031
+ return state.matches === Math.max(1, item.nth ?? 1);
2032
+ }
2033
+
2034
+ // src/internal/probe/mode.ts
2035
+ var planCache;
2036
+ function isProbeMode() {
2037
+ return process.env[PIWI_PROBE_ENV.flag] === "1" || process.env[PIWI_PROBE_ENV.flag] === "true";
2038
+ }
2039
+ function loadPlan() {
2040
+ if (planCache !== void 0) return planCache;
2041
+ planCache = null;
2042
+ const path6 = process.env[PIWI_PROBE_ENV.plan];
2043
+ if (path6) {
2044
+ try {
2045
+ planCache = JSON.parse(fs2.readFileSync(path6, "utf8"));
2046
+ } catch {
2047
+ planCache = null;
2048
+ }
2049
+ }
2050
+ return planCache;
2051
+ }
2052
+ function probeItemForTest(test) {
2053
+ const plan = loadPlan();
2054
+ if (!plan) return null;
2055
+ const filePath = test.file ? path3.relative(process.cwd(), test.file).split(path3.sep).join("/") : null;
2056
+ const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
2057
+ return matchProbeItem(plan, { title: test.title, filePath, suitePath });
2058
+ }
2059
+ function classifyProbeHandled(signals) {
2060
+ return signals.consoleErrors > 0 || signals.dialogs > 0 || signals.backendError ? "degraded" : "graceful";
2061
+ }
2062
+ function outcomeFromStatus(status, applied) {
2063
+ if (!applied) return "inconclusive";
2064
+ return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2065
+ }
2066
+ function recordProbeOutcome(line) {
2067
+ const path6 = process.env[PIWI_PROBE_ENV.results];
2068
+ if (!path6) return;
2069
+ try {
2070
+ fs2.appendFileSync(path6, `${JSON.stringify(line)}
2071
+ `);
2072
+ } catch {
2073
+ }
2074
+ }
2075
+
2076
+ // src/internal/probe/interception.ts
2077
+ var import_node_zlib = require("zlib");
2078
+
2079
+ // src/internal/probe/faults.ts
2080
+ var PROBE_FAULTS = ["status-500", "empty-body", "drop-field", "stale-value", "slow"];
2081
+ var SERVER_PROBE_FAULTS = [
2082
+ "throw",
2083
+ "status",
2084
+ "delay",
2085
+ "dependency",
2086
+ "data",
2087
+ "extreme",
2088
+ "replay",
2089
+ "auth",
2090
+ "slow-first"
2091
+ ];
2092
+ function isFaultAllowed(level, fault) {
2093
+ const list = level === "server" ? SERVER_PROBE_FAULTS : PROBE_FAULTS;
2094
+ return list.includes(fault);
2095
+ }
2096
+ var SLOW_FAULT_DELAY_MS = 5e3;
2097
+ function dropOneField(body) {
2098
+ let parsed;
2099
+ try {
2100
+ parsed = JSON.parse(body);
2101
+ } catch {
2102
+ return body;
2103
+ }
2104
+ if (Array.isArray(parsed)) {
2105
+ let key;
2106
+ for (const el of parsed) {
2107
+ if (el && typeof el === "object" && !Array.isArray(el)) {
2108
+ key ??= Object.keys(el)[0];
2109
+ if (key) delete el[key];
2110
+ }
2111
+ }
2112
+ return JSON.stringify(parsed);
2113
+ }
2114
+ if (parsed && typeof parsed === "object") {
2115
+ const obj = parsed;
2116
+ const key = Object.keys(obj)[0];
2117
+ if (key) delete obj[key];
2118
+ return JSON.stringify(obj);
2119
+ }
2120
+ return body;
2121
+ }
2122
+ function computeFault(fault, input) {
2123
+ const base = {
2124
+ status: input.status,
2125
+ body: input.body,
2126
+ contentType: input.contentType ?? null,
2127
+ delayMs: 0,
2128
+ changed: false
2129
+ };
2130
+ switch (fault) {
2131
+ case "status-500":
2132
+ return { ...base, status: 500, changed: input.status !== 500 };
2133
+ case "empty-body":
2134
+ return { ...base, body: "", changed: input.body.length > 0 };
2135
+ case "drop-field": {
2136
+ const body = dropOneField(input.body);
2137
+ return { ...base, body, changed: body !== input.body };
2138
+ }
2139
+ case "stale-value": {
2140
+ const body = input.priorBody ?? input.body;
2141
+ return { ...base, body, changed: body !== input.body };
2142
+ }
2143
+ case "slow":
2144
+ return { ...base, delayMs: SLOW_FAULT_DELAY_MS, changed: true };
2145
+ default:
2146
+ return base;
2147
+ }
2148
+ }
2149
+
2150
+ // src/internal/probe/sign.ts
2151
+ var import_node_crypto = require("crypto");
2152
+ function signProbeMessage(secret, nonce, ts, specJson) {
2153
+ return (0, import_node_crypto.createHmac)("sha256", secret).update(`${nonce}.${ts}.${specJson}`).digest("hex");
2154
+ }
2155
+ function buildProbeHeader(secret, spec, now = Date.now()) {
2156
+ const nonce = (0, import_node_crypto.randomBytes)(8).toString("hex");
2157
+ const specJson = JSON.stringify(spec);
2158
+ const sig = signProbeMessage(secret, nonce, now, specJson);
2159
+ return Buffer.from(JSON.stringify({ nonce, ts: now, specJson, sig }), "utf8").toString("base64");
2160
+ }
2161
+
2162
+ // src/internal/probe/interception.ts
2163
+ function parseProbeTrace(traceHeader) {
2164
+ const none = { appliedFault: null, serverError: false };
2165
+ if (!traceHeader) return none;
2166
+ try {
2167
+ const spans = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf8"));
2168
+ if (!Array.isArray(spans)) return none;
2169
+ const root = spans.find((s) => s && typeof s === "object" && !s.parentId);
2170
+ if (!root) return none;
2171
+ const attrs = root.attrs ?? {};
2172
+ const appliedRaw = attrs["piwi.probe.applied"];
2173
+ const status = Number(attrs["http.status_code"]) || 0;
2174
+ return {
2175
+ appliedFault: appliedRaw != null ? String(appliedRaw) : null,
2176
+ serverError: root.status === "error" || status >= 500
2177
+ };
2178
+ } catch {
2179
+ return none;
2180
+ }
2181
+ }
2182
+ function fulfillHeaders(responseHeaders, contentType) {
2183
+ const headers = { ...responseHeaders };
2184
+ delete headers["content-length"];
2185
+ delete headers["content-encoding"];
2186
+ if (contentType) headers["content-type"] = contentType;
2187
+ return headers;
2188
+ }
2189
+ function originOf(url) {
2190
+ try {
2191
+ const origin = new URL(url).origin;
2192
+ return origin && origin !== "null" ? origin : null;
2193
+ } catch {
2194
+ return null;
2195
+ }
2196
+ }
2197
+ async function installProbeInterception(page, item) {
2198
+ if (!isFaultAllowed(item.level ?? "client", item.fault)) {
2199
+ return { applied: () => false, appliedFault: () => null, serverError: () => false };
2200
+ }
2201
+ const state = createProbeState();
2202
+ let applied = false;
2203
+ let appliedFault = null;
2204
+ let serverError = false;
2205
+ let appOrigin = null;
2206
+ const probeSecret = process.env[PIWI_PROBE_ENV.secret];
2207
+ page.on("framenavigated", (frame) => {
2208
+ if (frame === page.mainFrame()) {
2209
+ markNavigated(state);
2210
+ const origin = originOf(page.url());
2211
+ if (origin) appOrigin = origin;
2212
+ }
2213
+ });
2214
+ await page.route("**/*", async (route) => {
2215
+ const request = route.request();
2216
+ if (applied || !requestMatchesRoute(request.method(), request.url(), item.routeKey) || !shouldMutate(state, item)) {
2217
+ await route.fallback();
2218
+ return;
2219
+ }
2220
+ if (item.level === "server") {
2221
+ if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
2222
+ await route.fallback();
2223
+ return;
2224
+ }
2225
+ const spec = { route: item.routeKey, fault: item.fault, nth: item.nth };
2226
+ if (item.dependency) spec.dependency = item.dependency;
2227
+ try {
2228
+ const response = await route.fetch({
2229
+ headers: { ...request.headers(), "x-piwi-probe": buildProbeHeader(probeSecret, spec) }
2230
+ });
2231
+ const trace = parseProbeTrace(response.headers()["x-piwi-trace"]);
2232
+ appliedFault = trace.appliedFault;
2233
+ serverError = trace.serverError;
2234
+ applied = trace.appliedFault != null && trace.appliedFault.split(":", 1)[0] === item.fault;
2235
+ await route.fulfill({ response });
2236
+ } catch {
2237
+ applied = false;
2238
+ await route.fallback();
2239
+ }
2240
+ return;
2241
+ }
2242
+ try {
2243
+ const response = await route.fetch();
2244
+ const body = await response.text();
2245
+ const out = computeFault(item.fault, {
2246
+ status: response.status(),
2247
+ body,
2248
+ contentType: response.headers()["content-type"] ?? null
2249
+ });
2250
+ applied = out.changed;
2251
+ if (applied) appliedFault = item.fault;
2252
+ if (out.delayMs > 0) await new Promise((resolve) => setTimeout(resolve, out.delayMs));
2253
+ const headers = fulfillHeaders(response.headers(), out.contentType);
2254
+ await route.fulfill({ status: out.status, body: out.body, headers });
2255
+ } catch {
2256
+ applied = false;
2257
+ await route.fallback();
2258
+ }
2259
+ });
2260
+ return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
2261
+ }
2262
+
1756
2263
  // src/internal/capture/inspect-on-failure.ts
1757
2264
  function isCi(ci) {
1758
2265
  return ci !== void 0 && ci !== "" && ci !== "false";
@@ -1789,88 +2296,295 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
1789
2296
  }
1790
2297
 
1791
2298
  // src/internal/capture/pick-on-failure.ts
1792
- var path2 = __toESM(require("path"));
1793
- var ANSI_RE = /\[[0-9;]*m/g;
1794
- function endOfString(s, start) {
1795
- const q = s[start];
1796
- for (let i = start + 1; i < s.length; i++) {
1797
- if (s[i] === "\\") {
1798
- i++;
1799
- continue;
1800
- }
1801
- if (s[i] === q) return i;
2299
+ var path4 = __toESM(require("path"));
2300
+
2301
+ // ../core/src/locator-chain.ts
2302
+ var LOCATING_METHODS = /* @__PURE__ */ new Set([
2303
+ "getByRole",
2304
+ "getByTestId",
2305
+ "getByText",
2306
+ "getByLabel",
2307
+ "getByPlaceholder",
2308
+ "getByAltText",
2309
+ "getByTitle",
2310
+ "locator",
2311
+ "frameLocator"
2312
+ ]);
2313
+ var NARROWING_METHODS = /* @__PURE__ */ new Set([
2314
+ "filter",
2315
+ "first",
2316
+ "last",
2317
+ "nth",
2318
+ "and",
2319
+ "or",
2320
+ "visible",
2321
+ "contentFrame",
2322
+ "owner"
2323
+ ]);
2324
+ var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
2325
+ var LocatorParseError = class extends Error {
2326
+ constructor(message) {
2327
+ super(message);
2328
+ this.name = "LocatorParseError";
1802
2329
  }
1803
- return s.length - 1;
1804
- }
1805
- function matchBrace(s, start) {
1806
- let depth = 0;
1807
- for (let i = start; i < s.length; i++) {
1808
- if (s[i] === "{") depth++;
1809
- else if (s[i] === "}" && --depth === 0) return i;
1810
- }
1811
- return s.length - 1;
1812
- }
1813
- function parseOptions(src) {
1814
- const obj = {};
1815
- const re = /(\w+)\s*:\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*"|true|false|-?\d+)/g;
1816
- let m;
1817
- while ((m = re.exec(src)) !== null) {
1818
- const key = m[1];
1819
- const raw = m[2];
1820
- if (raw === "true") obj[key] = true;
1821
- else if (raw === "false") obj[key] = false;
1822
- else if (/^-?\d+$/.test(raw)) obj[key] = Number(raw);
1823
- else obj[key] = raw.slice(1, -1).replace(/\\(.)/g, "$1");
1824
- }
1825
- return obj;
1826
- }
1827
- function parseArgs(inner) {
1828
- const args = [];
1829
- let i = 0;
1830
- while (i < inner.length) {
1831
- const c = inner[i];
1832
- if (c === " " || c === ",") {
1833
- i++;
1834
- continue;
1835
- }
1836
- if (c === "'" || c === '"') {
1837
- const end = endOfString(inner, i);
1838
- args.push(inner.slice(i + 1, end).replace(/\\(.)/g, "$1"));
1839
- i = end + 1;
1840
- continue;
2330
+ };
2331
+ var SIMPLE_ESCAPES = {
2332
+ n: "\n",
2333
+ t: " ",
2334
+ r: "\r",
2335
+ b: "\b",
2336
+ f: "\f",
2337
+ v: "\v",
2338
+ "0": "\0"
2339
+ };
2340
+ var Parser = class {
2341
+ constructor(src) {
2342
+ this.src = src;
2343
+ this.i = 0;
2344
+ }
2345
+ parseTopLevel() {
2346
+ const chain = this.parseChain();
2347
+ this.skipSpace();
2348
+ if (this.i < this.src.length) this.fail("unexpected text");
2349
+ return chain;
2350
+ }
2351
+ fail(message) {
2352
+ throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
2353
+ }
2354
+ skipSpace() {
2355
+ while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
2356
+ }
2357
+ peek() {
2358
+ return this.src[this.i];
2359
+ }
2360
+ expect(ch) {
2361
+ this.skipSpace();
2362
+ if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
2363
+ this.i++;
2364
+ }
2365
+ identifier() {
2366
+ const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
2367
+ if (!m) this.fail("expected a name");
2368
+ this.i += m[0].length;
2369
+ return m[0];
2370
+ }
2371
+ /**
2372
+ * A chain of calls joined by `.`. With `lenient`, parsing stops before the
2373
+ * first link that doesn't parse instead of failing, so a chain followed by
2374
+ * other text (an error message) is read up to where it ends. `spans` receives
2375
+ * where each top-level call starts and ends.
2376
+ */
2377
+ parseChain(opts = {}) {
2378
+ this.skipSpace();
2379
+ const calls = [];
2380
+ for (; ; ) {
2381
+ const before = this.i;
2382
+ if (calls.length > 0) this.i++;
2383
+ this.skipSpace();
2384
+ const start = this.i;
2385
+ try {
2386
+ calls.push(this.parseCall(calls.length === 0));
2387
+ } catch (error) {
2388
+ if (!opts.lenient || calls.length === 0) throw error;
2389
+ this.i = before;
2390
+ break;
2391
+ }
2392
+ opts.spans?.push({ start, end: this.i });
2393
+ const end = this.i;
2394
+ this.skipSpace();
2395
+ if (this.peek() !== ".") {
2396
+ this.i = end;
2397
+ break;
2398
+ }
1841
2399
  }
1842
- if (c === "{") {
1843
- const end = matchBrace(inner, i);
1844
- args.push(parseOptions(inner.slice(i, end + 1)));
1845
- i = end + 1;
1846
- continue;
2400
+ return { calls };
2401
+ }
2402
+ /** Where the parser stopped. */
2403
+ get position() {
2404
+ return this.i;
2405
+ }
2406
+ parseCall(first) {
2407
+ this.skipSpace();
2408
+ const method = this.identifier();
2409
+ if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
2410
+ if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
2411
+ this.expect("(");
2412
+ const args = [];
2413
+ this.skipSpace();
2414
+ if (this.peek() === ")") {
2415
+ this.i++;
2416
+ return { method, args };
2417
+ }
2418
+ for (; ; ) {
2419
+ args.push(this.parseValue());
2420
+ this.skipSpace();
2421
+ if (this.peek() === ",") {
2422
+ this.i++;
2423
+ this.skipSpace();
2424
+ if (this.peek() === ")") {
2425
+ this.i++;
2426
+ break;
2427
+ }
2428
+ continue;
2429
+ }
2430
+ this.expect(")");
2431
+ break;
2432
+ }
2433
+ return { method, args };
2434
+ }
2435
+ parseValue() {
2436
+ this.skipSpace();
2437
+ const ch = this.peek();
2438
+ if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
2439
+ if (ch === "/") return this.parseRegex();
2440
+ if (ch === "{") return this.parseObject();
2441
+ if (ch !== void 0 && /[-\d]/.test(ch)) {
2442
+ const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
2443
+ if (!m) this.fail("expected a number");
2444
+ this.i += m[0].length;
2445
+ return { type: "number", value: Number(m[0]) };
2446
+ }
2447
+ const rest = this.src.slice(this.i);
2448
+ if (/^true\b/.test(rest)) {
2449
+ this.i += 4;
2450
+ return { type: "boolean", value: true };
2451
+ }
2452
+ if (/^false\b/.test(rest)) {
2453
+ this.i += 5;
2454
+ return { type: "boolean", value: false };
2455
+ }
2456
+ if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
2457
+ return this.fail("expected a value");
2458
+ }
2459
+ /** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
2460
+ parseString() {
2461
+ const quote = this.src[this.i];
2462
+ let out = "";
2463
+ this.i++;
2464
+ while (this.i < this.src.length) {
2465
+ const c = this.src[this.i];
2466
+ if (c === quote) {
2467
+ this.i++;
2468
+ return out;
2469
+ }
2470
+ if (c !== "\\") {
2471
+ out += c;
2472
+ this.i++;
2473
+ continue;
2474
+ }
2475
+ const next = this.src[this.i + 1];
2476
+ if (next === void 0) break;
2477
+ const simple = SIMPLE_ESCAPES[next];
2478
+ if (simple !== void 0) {
2479
+ out += simple;
2480
+ this.i += 2;
2481
+ continue;
2482
+ }
2483
+ const hex = next === "u" && this.src[this.i + 2] === "{" ? /^u\{([0-9a-fA-F]{1,6})\}/.exec(this.src.slice(this.i + 1)) : next === "u" ? /^u([0-9a-fA-F]{4})/.exec(this.src.slice(this.i + 1)) : next === "x" ? /^x([0-9a-fA-F]{2})/.exec(this.src.slice(this.i + 1)) : null;
2484
+ if (hex) {
2485
+ out += String.fromCodePoint(parseInt(hex[1], 16));
2486
+ this.i += 1 + hex[0].length;
2487
+ continue;
2488
+ }
2489
+ if (next === "u" || next === "x") this.fail("malformed escape");
2490
+ out += next;
2491
+ this.i += 2;
2492
+ }
2493
+ return this.fail("unterminated string");
2494
+ }
2495
+ parseRegex() {
2496
+ let j = this.i + 1;
2497
+ let inClass = false;
2498
+ while (j < this.src.length) {
2499
+ const c = this.src[j];
2500
+ if (c === "\\") {
2501
+ j += 2;
2502
+ continue;
2503
+ }
2504
+ if (c === "[") inClass = true;
2505
+ else if (c === "]") inClass = false;
2506
+ else if (c === "/" && !inClass) break;
2507
+ j++;
2508
+ }
2509
+ if (j >= this.src.length) this.fail("unterminated regex");
2510
+ const source = this.src.slice(this.i + 1, j);
2511
+ const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
2512
+ this.i = j + 1 + flags.length;
2513
+ return { type: "regex", source, flags };
2514
+ }
2515
+ parseObject() {
2516
+ this.expect("{");
2517
+ const entries = [];
2518
+ for (; ; ) {
2519
+ this.skipSpace();
2520
+ if (this.peek() === "}") {
2521
+ this.i++;
2522
+ break;
2523
+ }
2524
+ const ch = this.peek();
2525
+ const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
2526
+ this.expect(":");
2527
+ entries.push([key, this.parseValue()]);
2528
+ this.skipSpace();
2529
+ if (this.peek() === ",") {
2530
+ this.i++;
2531
+ continue;
2532
+ }
2533
+ this.expect("}");
2534
+ break;
1847
2535
  }
1848
- i++;
2536
+ return { type: "object", entries };
1849
2537
  }
1850
- return args;
2538
+ };
2539
+ function parseLocatorChain(expr) {
2540
+ const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
2541
+ if (!trimmed) throw new LocatorParseError("empty expression");
2542
+ return new Parser(trimmed).parseTopLevel();
1851
2543
  }
1852
- function leafExpression(expr) {
1853
- let depth = 0;
1854
- let leafStart = 0;
1855
- for (let i = 0; i < expr.length - 1; i++) {
1856
- const c = expr[i];
1857
- if (c === "'" || c === '"') {
1858
- i = endOfString(expr, i);
1859
- continue;
1860
- }
1861
- if (c === "(") depth++;
1862
- else if (c === ")") {
1863
- depth--;
1864
- if (depth === 0 && expr[i + 1] === ".") leafStart = i + 2;
1865
- }
2544
+ function tryParseLocatorChain(expr) {
2545
+ try {
2546
+ return parseLocatorChain(expr);
2547
+ } catch {
2548
+ return null;
1866
2549
  }
1867
- return expr.slice(leafStart);
1868
2550
  }
2551
+ function parseLeafLocatorCall(expr) {
2552
+ const chain = tryParseLocatorChain(expr);
2553
+ if (!chain) return null;
2554
+ for (let i = chain.calls.length - 1; i >= 0; i--) {
2555
+ if (LOCATING_METHODS.has(chain.calls[i].method)) return chain.calls[i];
2556
+ }
2557
+ return null;
2558
+ }
2559
+ function locatorCallValues(call, opts = {}) {
2560
+ const value = (arg) => {
2561
+ switch (arg.type) {
2562
+ case "string":
2563
+ case "number":
2564
+ case "boolean":
2565
+ return arg.value;
2566
+ case "regex":
2567
+ return opts.regexAsText ? `/${arg.source}/${arg.flags}` : void 0;
2568
+ case "object": {
2569
+ const out = {};
2570
+ for (const [key, v] of arg.entries) {
2571
+ const plain = value(v);
2572
+ if (plain !== void 0) out[key] = plain;
2573
+ }
2574
+ return out;
2575
+ }
2576
+ case "chain":
2577
+ return void 0;
2578
+ }
2579
+ };
2580
+ return call.args.map(value).filter((v) => v !== void 0);
2581
+ }
2582
+
2583
+ // src/internal/capture/pick-on-failure.ts
2584
+ var ANSI_RE = /\[[0-9;]*m/g;
1869
2585
  function parseLeafLocatorExpression(rawExpr) {
1870
- const expr = leafExpression(rawExpr.trim());
1871
- const m = /^([A-Za-z]+)\((.*)\)$/s.exec(expr);
1872
- if (!m) return null;
1873
- return { method: m[1], args: parseArgs(m[2].trim()) };
2586
+ const leaf = parseLeafLocatorCall(rawExpr);
2587
+ return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
1874
2588
  }
1875
2589
  function deriveFailedLocator(testInfo) {
1876
2590
  const info = testInfo;
@@ -1883,7 +2597,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE, "");
1883
2597
  const parsed = parseLeafLocatorExpression(line[1].trim());
1884
2598
  if (!parsed) continue;
1885
2599
  const loc = err.location;
1886
- const location = loc ? `${path2.relative(process.cwd(), loc.file).split(path2.sep).join("/")}:${loc.line}:${loc.column}` : null;
2600
+ const location = loc ? `${path4.relative(process.cwd(), loc.file).split(path4.sep).join("/")}:${loc.line}:${loc.column}` : null;
1887
2601
  return { method: parsed.method, args: parsed.args, location };
1888
2602
  }
1889
2603
  return null;
@@ -2032,13 +2746,13 @@ function applyPickToSnapshots(snapshots, pick) {
2032
2746
  }
2033
2747
 
2034
2748
  // src/internal/support/aria-sampling.ts
2035
- var path3 = __toESM(require("path"));
2036
- var os2 = __toESM(require("os"));
2037
- var fs = __toESM(require("fs"));
2749
+ var path5 = __toESM(require("path"));
2750
+ var os3 = __toESM(require("os"));
2751
+ var fs3 = __toESM(require("fs"));
2038
2752
 
2039
2753
  // src/internal/support/instance-id.ts
2040
2754
  var crypto = __toESM(require("crypto"));
2041
- var os = __toESM(require("os"));
2755
+ var os2 = __toESM(require("os"));
2042
2756
  function hashForProject(projectName) {
2043
2757
  return crypto.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
2044
2758
  }
@@ -2048,14 +2762,14 @@ function ariaSampleIdentity(filePath, title) {
2048
2762
  return `${filePath}\0${title}`;
2049
2763
  }
2050
2764
  function getAriaSampleFilePath(projectName) {
2051
- return path3.join(os2.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
2765
+ return path5.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
2052
2766
  }
2053
2767
  var cachedSets = /* @__PURE__ */ new Map();
2054
2768
  function loadAriaSampleSet(projectName) {
2055
2769
  if (cachedSets.has(projectName)) return cachedSets.get(projectName);
2056
2770
  let set = null;
2057
2771
  try {
2058
- const raw = fs.readFileSync(getAriaSampleFilePath(projectName), "utf8");
2772
+ const raw = fs3.readFileSync(getAriaSampleFilePath(projectName), "utf8");
2059
2773
  const parsed = JSON.parse(raw);
2060
2774
  if (parsed.projectName === projectName && Array.isArray(parsed.identities)) {
2061
2775
  set = new Set(parsed.identities.filter((x) => typeof x === "string"));
@@ -2067,7 +2781,7 @@ function loadAriaSampleSet(projectName) {
2067
2781
  return set;
2068
2782
  }
2069
2783
  function relativeTestFile(file) {
2070
- return path3.relative(process.cwd(), file).split(path3.sep).join("/");
2784
+ return path5.relative(process.cwd(), file).split(path5.sep).join("/");
2071
2785
  }
2072
2786
  function isDueForAriaSample(testInfo) {
2073
2787
  const projectName = process.env.PIWI_PROJECT_NAME;
@@ -2123,6 +2837,9 @@ function createSink() {
2123
2837
  stashedPageState: null,
2124
2838
  stashedAria: null,
2125
2839
  stashedAriaJson: null,
2840
+ pageInventories: [],
2841
+ probeItem: null,
2842
+ probeInterception: null,
2126
2843
  pickOffered: false,
2127
2844
  userPick: null
2128
2845
  };
@@ -2299,6 +3016,62 @@ async function readPageState(page) {
2299
3016
  return null;
2300
3017
  }
2301
3018
  }
3019
+ var inventoriedPageKeys = /* @__PURE__ */ new Set();
3020
+ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
3021
+ var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
3022
+ var MAX_PAGE_INVENTORY_WINDOWS = 64;
3023
+ function withTimeout(promise, ms) {
3024
+ return new Promise((resolve, reject) => {
3025
+ const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
3026
+ promise.then(
3027
+ (value) => {
3028
+ clearTimeout(timer);
3029
+ resolve(value);
3030
+ },
3031
+ (error) => {
3032
+ clearTimeout(timer);
3033
+ reject(error);
3034
+ }
3035
+ );
3036
+ });
3037
+ }
3038
+ async function readPageInventory(page) {
3039
+ try {
3040
+ const inventory = await withTimeout(
3041
+ page.evaluate(collectPageInventoryInPage, PAGE_INVENTORY_IN_PAGE_CAP),
3042
+ PAGE_INVENTORY_EVAL_TIMEOUT_MS
3043
+ );
3044
+ return inventory ? { ...inventory, capturedAt: Date.now() } : null;
3045
+ } catch {
3046
+ return null;
3047
+ }
3048
+ }
3049
+ async function recordPageWindow(sink, page) {
3050
+ if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
3051
+ let url;
3052
+ try {
3053
+ url = page.url();
3054
+ } catch {
3055
+ return;
3056
+ }
3057
+ if (!url || url.startsWith("about:")) return;
3058
+ const key = inventoryPageKey(url);
3059
+ const capturedAt = Date.now();
3060
+ if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
3061
+ if (key && !inventoriedPageKeys.has(key)) {
3062
+ inventoriedPageKeys.add(key);
3063
+ const content = await readPageInventory(page);
3064
+ if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
3065
+ sink.pageInventories.push({
3066
+ url: content?.url ?? url,
3067
+ controls: content?.controls ?? [],
3068
+ links: content?.links ?? [],
3069
+ capturedAt
3070
+ });
3071
+ return;
3072
+ }
3073
+ sink.pageInventories.push({ url, controls: [], links: [], capturedAt });
3074
+ }
2302
3075
  async function stashPageState(sink, closing) {
2303
3076
  const page = sink.lastActivePage;
2304
3077
  if (!page || isPageClosed(page)) return;
@@ -2323,6 +3096,9 @@ async function stashPageState(sink, closing) {
2323
3096
  } else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
2324
3097
  await sampleAria();
2325
3098
  }
3099
+ if (status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
3100
+ await recordPageWindow(sink, page);
3101
+ }
2326
3102
  }
2327
3103
  async function maybeOpenPicker(sink, closing) {
2328
3104
  if (sink.pickOffered) return;
@@ -2566,6 +3342,13 @@ function wrapFrameLocator(page, frameLocator) {
2566
3342
  function instrumentPage(page) {
2567
3343
  if (!page || INSTRUMENTED_PAGES.has(page)) return;
2568
3344
  INSTRUMENTED_PAGES.add(page);
3345
+ const probeSink = currentSink;
3346
+ if (probeSink?.probeItem && !probeSink.probeInterception) {
3347
+ void installProbeInterception(page, probeSink.probeItem).then((interception) => {
3348
+ probeSink.probeInterception = interception;
3349
+ }).catch(() => {
3350
+ });
3351
+ }
2569
3352
  const ctx = pageContext(page);
2570
3353
  if (ctx) instrumentContext(ctx);
2571
3354
  if (typeof page.close === "function") {
@@ -2601,6 +3384,14 @@ function instrumentPage(page) {
2601
3384
  page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
2602
3385
  }
2603
3386
  }
3387
+ if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
3388
+ page.on("framenavigated", (frame) => {
3389
+ const sink = currentSink;
3390
+ if (!sink || frame !== page.mainFrame()) return;
3391
+ void recordPageWindow(sink, page).catch(() => {
3392
+ });
3393
+ });
3394
+ }
2604
3395
  page.on("console", (msg) => {
2605
3396
  const sink = currentSink;
2606
3397
  if (!sink) return;
@@ -2660,14 +3451,14 @@ function instrumentPage(page) {
2660
3451
  const logHeader = headers["x-piwi-logs"];
2661
3452
  if (logHeader) {
2662
3453
  try {
2663
- entry.serverLogs = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
3454
+ entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
2664
3455
  } catch {
2665
3456
  }
2666
3457
  }
2667
3458
  const traceHeader = headers["x-piwi-trace"];
2668
3459
  if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
2669
3460
  try {
2670
- entry.serverTraces = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
3461
+ entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
2671
3462
  } catch {
2672
3463
  }
2673
3464
  }
@@ -2852,6 +3643,16 @@ async function flushSink(sink, testInfo) {
2852
3643
  });
2853
3644
  }
2854
3645
  }
3646
+ if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
3647
+ if (pageReadable) await recordPageWindow(sink, page);
3648
+ const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
3649
+ if (windows.length > 0) {
3650
+ await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
3651
+ contentType: "application/json",
3652
+ body: Buffer.from(JSON.stringify(windows))
3653
+ });
3654
+ }
3655
+ }
2855
3656
  }
2856
3657
  var piwiFixtures = {
2857
3658
  // Worker-scoped: patch the shared browser so every page/context created from
@@ -2877,11 +3678,36 @@ var piwiFixtures = {
2877
3678
  async ({}, use, testInfo) => {
2878
3679
  const sink = createSink();
2879
3680
  sink.testInfo = testInfo;
3681
+ if (isProbeMode())
3682
+ sink.probeItem = probeItemForTest({
3683
+ title: testInfo.title,
3684
+ file: testInfo.file,
3685
+ titlePath: testInfo.titlePath
3686
+ });
2880
3687
  currentSink = sink;
2881
3688
  try {
2882
3689
  await use();
2883
3690
  } finally {
2884
3691
  currentSink = null;
3692
+ if (sink.probeItem) {
3693
+ const applied = sink.probeInterception?.applied() ?? false;
3694
+ const level = sink.probeItem.level ?? "client";
3695
+ const handled = level === "server" ? classifyProbeHandled({
3696
+ consoleErrors: sink.consoleEntries.filter((e) => e.type === "error").length,
3697
+ dialogs: sink.dialogs.length,
3698
+ backendError: sink.probeInterception?.serverError() ?? false
3699
+ }) : "n/a";
3700
+ recordProbeOutcome({
3701
+ testCaseId: sink.probeItem.testCaseId,
3702
+ routeKey: sink.probeItem.routeKey,
3703
+ fault: sink.probeItem.fault,
3704
+ applied,
3705
+ outcome: outcomeFromStatus(testInfo.status, applied),
3706
+ level,
3707
+ dependency: sink.probeItem.dependency ?? null,
3708
+ handled
3709
+ });
3710
+ }
2885
3711
  await flushSink(sink, testInfo);
2886
3712
  }
2887
3713
  },