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