@piwitests/reporter 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli/index.js +5310 -1221
- package/dist/global-setup-module.js +23 -5
- package/dist/index.d.ts +21 -0
- package/dist/index.js +3580 -2331
- package/dist/internal/capture/attachments.d.ts +1 -0
- package/dist/internal/capture/attachments.js +1 -0
- package/dist/internal/capture/capture-fixtures.d.ts +29 -2
- package/dist/internal/capture/capture-fixtures.js +1179 -102
- package/dist/internal/capture/locator-healing.js +2 -1
- package/dist/internal/capture/pick-on-failure.js +8 -1
- package/package.json +1 -1
- package/templates/skills/fix-a-reported-bug/SKILL.md +47 -0
- package/templates/skills/investigate-failure/SKILL.md +1 -1
- package/templates/skills/run-the-right-tests/SKILL.md +13 -1
- package/templates/skills/setup-piwi/SKILL.md +1 -1
- package/templates/skills/stabilize-flaky-tests/SKILL.md +30 -15
|
@@ -37,15 +37,22 @@ __export(capture_fixtures_exports, {
|
|
|
37
37
|
boxCaptureFrames: () => boxCaptureFrames,
|
|
38
38
|
buildPageState: () => buildPageState,
|
|
39
39
|
callMethod: () => callMethod,
|
|
40
|
+
codeReachRoots: () => codeReachRoots,
|
|
40
41
|
computeCoreVitals: () => computeCoreVitals,
|
|
42
|
+
configuredCodeReachRoots: () => configuredCodeReachRoots,
|
|
41
43
|
extendPiwiFixtures: () => extendPiwiFixtures,
|
|
42
44
|
internalCall: () => internalCall,
|
|
43
45
|
noteLocatorCall: () => noteLocatorCall,
|
|
44
46
|
noteNavigation: () => noteNavigation,
|
|
47
|
+
pageMapFetcher: () => pageMapFetcher,
|
|
45
48
|
piwiFixtures: () => piwiFixtures,
|
|
46
|
-
probeElementAttrs: () => probeElementAttrs
|
|
49
|
+
probeElementAttrs: () => probeElementAttrs,
|
|
50
|
+
resolveCodeReach: () => resolveCodeReach,
|
|
51
|
+
startCodeReach: () => startCodeReach,
|
|
52
|
+
stopCodeReach: () => stopCodeReach
|
|
47
53
|
});
|
|
48
54
|
module.exports = __toCommonJS(capture_fixtures_exports);
|
|
55
|
+
var path10 = __toESM(require("path"));
|
|
49
56
|
var import_node_zlib2 = require("zlib");
|
|
50
57
|
|
|
51
58
|
// ../picker-dom/src/probe.ts
|
|
@@ -521,6 +528,11 @@ function installPickerOverlay(arg) {
|
|
|
521
528
|
placeLabel(r);
|
|
522
529
|
foot.textContent = "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip";
|
|
523
530
|
};
|
|
531
|
+
const redescribe = () => {
|
|
532
|
+
labeled = null;
|
|
533
|
+
if (current()) refresh();
|
|
534
|
+
};
|
|
535
|
+
g.__piwiRedescribe = redescribe;
|
|
524
536
|
const stop = (e) => {
|
|
525
537
|
e.preventDefault();
|
|
526
538
|
e.stopImmediatePropagation();
|
|
@@ -575,6 +587,7 @@ function installPickerOverlay(arg) {
|
|
|
575
587
|
highlight.remove();
|
|
576
588
|
label.remove();
|
|
577
589
|
banner.remove();
|
|
590
|
+
if (g.__piwiRedescribe === redescribe) delete g.__piwiRedescribe;
|
|
578
591
|
};
|
|
579
592
|
const reportPicked = (el) => {
|
|
580
593
|
removePickingListeners();
|
|
@@ -1121,8 +1134,9 @@ function isAutoGenerated(value) {
|
|
|
1121
1134
|
}
|
|
1122
1135
|
if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
|
|
1123
1136
|
if (value.startsWith("ng-")) return true;
|
|
1124
|
-
if (/^(radix-|headlessui-|mui-|mantine-|chakra-)/i.test(value)) return true;
|
|
1137
|
+
if (/^(radix-|headlessui-|mui-|mantine-|chakra-|reka-|bits-|downshift-|react-aria|el-id-)/i.test(value)) return true;
|
|
1125
1138
|
if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
|
|
1139
|
+
if (/(^|[-_:])v-\d+(-\d+)+$/.test(value) || /(^|[-_:])v-\d+(-\d+)+[-_:]/.test(value)) return true;
|
|
1126
1140
|
return false;
|
|
1127
1141
|
}
|
|
1128
1142
|
var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
|
|
@@ -1778,6 +1792,7 @@ var ATTACHMENT_NAMES = {
|
|
|
1778
1792
|
network: "piwi-network",
|
|
1779
1793
|
pageInventory: "piwi-page-inventory",
|
|
1780
1794
|
locatorPages: "piwi-locator-pages",
|
|
1795
|
+
codeReach: "piwi-code-reach",
|
|
1781
1796
|
webVitals: "piwi-web-vitals",
|
|
1782
1797
|
locatorSuggestion: "piwi-locator-suggestion",
|
|
1783
1798
|
pageState: "piwi-page-state",
|
|
@@ -1938,6 +1953,11 @@ function pageKey(url) {
|
|
|
1938
1953
|
const q = normalized.indexOf("?");
|
|
1939
1954
|
return q < 0 ? normalized : normalized.slice(0, q);
|
|
1940
1955
|
}
|
|
1956
|
+
function requestRouteKey(method, url) {
|
|
1957
|
+
const route = normalizeRoute(url ?? "");
|
|
1958
|
+
const q = route.indexOf("?");
|
|
1959
|
+
return `${(method || "GET").toUpperCase()} ${q === -1 ? route : route.slice(0, q)}`;
|
|
1960
|
+
}
|
|
1941
1961
|
|
|
1942
1962
|
// src/internal/capture/locator-pages.ts
|
|
1943
1963
|
var MAX_LOCATOR_PAGE_ENTRIES = 1e3;
|
|
@@ -2015,6 +2035,8 @@ var DEFAULTS = {
|
|
|
2015
2035
|
// Off by default: the page inventory reads control and link names from every
|
|
2016
2036
|
// visited page, so it stays opt-in until a project turns it on.
|
|
2017
2037
|
capturePageInventory: false,
|
|
2038
|
+
// Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
|
|
2039
|
+
captureCodeReach: false,
|
|
2018
2040
|
captureServerTraces: true,
|
|
2019
2041
|
sampleAriaOnPass: true,
|
|
2020
2042
|
uploadManifest: true,
|
|
@@ -2051,6 +2073,8 @@ var PIWI_ENV_KEYS = {
|
|
|
2051
2073
|
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
2052
2074
|
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
2053
2075
|
capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
|
|
2076
|
+
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
2077
|
+
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
2054
2078
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
2055
2079
|
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
2056
2080
|
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
@@ -2073,6 +2097,10 @@ var PIWI_PROBE_ENV = {
|
|
|
2073
2097
|
results: "PIWI_PROBE_RESULTS",
|
|
2074
2098
|
secret: "PIWI_PROBE_SECRET"
|
|
2075
2099
|
};
|
|
2100
|
+
var PIWI_FLAKE_ENV = {
|
|
2101
|
+
plan: "PIWI_FLAKE_PLAN",
|
|
2102
|
+
results: "PIWI_FLAKE_RESULTS"
|
|
2103
|
+
};
|
|
2076
2104
|
var ENV_FALLBACK_SPECS = [
|
|
2077
2105
|
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
2078
2106
|
{ option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
|
|
@@ -2094,6 +2122,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
2094
2122
|
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
2095
2123
|
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
2096
2124
|
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
2125
|
+
{ option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
|
|
2097
2126
|
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
2098
2127
|
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
2099
2128
|
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
@@ -2104,14 +2133,14 @@ var ENV_FALLBACK_SPECS = [
|
|
|
2104
2133
|
];
|
|
2105
2134
|
|
|
2106
2135
|
// src/internal/probe/plan.ts
|
|
2107
|
-
function parseRouteKey(
|
|
2108
|
-
const space =
|
|
2109
|
-
if (space < 0) return { method: "", pattern:
|
|
2110
|
-
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) };
|
|
2111
2140
|
}
|
|
2112
2141
|
function patternToRegex(pattern) {
|
|
2113
|
-
const
|
|
2114
|
-
const escaped =
|
|
2142
|
+
const path11 = pattern.split("?")[0] ?? pattern;
|
|
2143
|
+
const escaped = path11.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
2115
2144
|
return new RegExp(`^${escaped}$`);
|
|
2116
2145
|
}
|
|
2117
2146
|
function suitePathEqual(a, b) {
|
|
@@ -2131,16 +2160,16 @@ function matchProbeItem(plan, test) {
|
|
|
2131
2160
|
}
|
|
2132
2161
|
return null;
|
|
2133
2162
|
}
|
|
2134
|
-
function requestMatchesRoute(method, url,
|
|
2135
|
-
const { method: rm, pattern } = parseRouteKey(
|
|
2163
|
+
function requestMatchesRoute(method, url, routeKey2) {
|
|
2164
|
+
const { method: rm, pattern } = parseRouteKey(routeKey2);
|
|
2136
2165
|
if (rm && rm !== method.toUpperCase()) return false;
|
|
2137
|
-
let
|
|
2166
|
+
let path11;
|
|
2138
2167
|
try {
|
|
2139
|
-
|
|
2168
|
+
path11 = new URL(url).pathname;
|
|
2140
2169
|
} catch {
|
|
2141
|
-
|
|
2170
|
+
path11 = url.split("?")[0] ?? url;
|
|
2142
2171
|
}
|
|
2143
|
-
return patternToRegex(pattern).test(
|
|
2172
|
+
return patternToRegex(pattern).test(path11);
|
|
2144
2173
|
}
|
|
2145
2174
|
function createProbeState() {
|
|
2146
2175
|
return { navigated: false, matches: 0 };
|
|
@@ -2148,10 +2177,10 @@ function createProbeState() {
|
|
|
2148
2177
|
function markNavigated(state) {
|
|
2149
2178
|
state.navigated = true;
|
|
2150
2179
|
}
|
|
2151
|
-
function
|
|
2180
|
+
function shouldAct(state, match) {
|
|
2152
2181
|
if (!state.navigated) return false;
|
|
2153
2182
|
state.matches += 1;
|
|
2154
|
-
return state.matches === Math.max(1,
|
|
2183
|
+
return match === "all" || state.matches === Math.max(1, match);
|
|
2155
2184
|
}
|
|
2156
2185
|
|
|
2157
2186
|
// src/internal/probe/mode.ts
|
|
@@ -2162,10 +2191,10 @@ function isProbeMode() {
|
|
|
2162
2191
|
function loadPlan() {
|
|
2163
2192
|
if (planCache !== void 0) return planCache;
|
|
2164
2193
|
planCache = null;
|
|
2165
|
-
const
|
|
2166
|
-
if (
|
|
2194
|
+
const path11 = process.env[PIWI_PROBE_ENV.plan];
|
|
2195
|
+
if (path11) {
|
|
2167
2196
|
try {
|
|
2168
|
-
planCache = JSON.parse(fs2.readFileSync(
|
|
2197
|
+
planCache = JSON.parse(fs2.readFileSync(path11, "utf8"));
|
|
2169
2198
|
} catch {
|
|
2170
2199
|
planCache = null;
|
|
2171
2200
|
}
|
|
@@ -2187,10 +2216,10 @@ function outcomeFromStatus(status, applied) {
|
|
|
2187
2216
|
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
2188
2217
|
}
|
|
2189
2218
|
function recordProbeOutcome(line) {
|
|
2190
|
-
const
|
|
2191
|
-
if (!
|
|
2219
|
+
const path11 = process.env[PIWI_PROBE_ENV.results];
|
|
2220
|
+
if (!path11) return;
|
|
2192
2221
|
try {
|
|
2193
|
-
fs2.appendFileSync(
|
|
2222
|
+
fs2.appendFileSync(path11, `${JSON.stringify(line)}
|
|
2194
2223
|
`);
|
|
2195
2224
|
} catch {
|
|
2196
2225
|
}
|
|
@@ -2217,6 +2246,9 @@ function isFaultAllowed(level, fault) {
|
|
|
2217
2246
|
return list.includes(fault);
|
|
2218
2247
|
}
|
|
2219
2248
|
var SLOW_FAULT_DELAY_MS = 5e3;
|
|
2249
|
+
function faultRouteAction(fault) {
|
|
2250
|
+
return fault === "slow" ? { kind: "delay", ms: SLOW_FAULT_DELAY_MS } : null;
|
|
2251
|
+
}
|
|
2220
2252
|
function dropOneField(body) {
|
|
2221
2253
|
let parsed;
|
|
2222
2254
|
try {
|
|
@@ -2317,11 +2349,46 @@ function originOf(url) {
|
|
|
2317
2349
|
return null;
|
|
2318
2350
|
}
|
|
2319
2351
|
}
|
|
2352
|
+
var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, Math.max(0, ms)));
|
|
2353
|
+
async function performRouteAction(route, action, startedAt) {
|
|
2354
|
+
switch (action.kind) {
|
|
2355
|
+
case "abort":
|
|
2356
|
+
await route.abort("connectionreset");
|
|
2357
|
+
return;
|
|
2358
|
+
case "status":
|
|
2359
|
+
await route.fulfill({ status: action.status, body: "" });
|
|
2360
|
+
return;
|
|
2361
|
+
case "delay": {
|
|
2362
|
+
const response = await route.fetch();
|
|
2363
|
+
await sleep(startedAt + action.ms - Date.now());
|
|
2364
|
+
await route.fulfill({ response });
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
async function installRouteRules(page, rules) {
|
|
2370
|
+
const state = rules.map(() => createProbeState());
|
|
2371
|
+
page.on("framenavigated", (frame) => {
|
|
2372
|
+
if (frame === page.mainFrame()) for (const s of state) markNavigated(s);
|
|
2373
|
+
});
|
|
2374
|
+
await page.route("**/*", async (route) => {
|
|
2375
|
+
const startedAt = Date.now();
|
|
2376
|
+
const request = route.request();
|
|
2377
|
+
for (let i = 0; i < rules.length; i++) {
|
|
2378
|
+
const rule = rules[i];
|
|
2379
|
+
if (rule.done?.() || !rule.matches(request.method(), request.url())) continue;
|
|
2380
|
+
if (!shouldAct(state[i], rule.match)) continue;
|
|
2381
|
+
await rule.handle(route, startedAt);
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
await route.fallback();
|
|
2385
|
+
});
|
|
2386
|
+
return { state };
|
|
2387
|
+
}
|
|
2320
2388
|
async function installProbeInterception(page, item) {
|
|
2321
2389
|
if (!isFaultAllowed(item.level ?? "client", item.fault)) {
|
|
2322
2390
|
return { applied: () => false, appliedFault: () => null, serverError: () => false };
|
|
2323
2391
|
}
|
|
2324
|
-
const state = createProbeState();
|
|
2325
2392
|
let applied = false;
|
|
2326
2393
|
let appliedFault = null;
|
|
2327
2394
|
let serverError = false;
|
|
@@ -2329,17 +2396,12 @@ async function installProbeInterception(page, item) {
|
|
|
2329
2396
|
const probeSecret = process.env[PIWI_PROBE_ENV.secret];
|
|
2330
2397
|
page.on("framenavigated", (frame) => {
|
|
2331
2398
|
if (frame === page.mainFrame()) {
|
|
2332
|
-
markNavigated(state);
|
|
2333
2399
|
const origin = originOf(page.url());
|
|
2334
2400
|
if (origin) appOrigin = origin;
|
|
2335
2401
|
}
|
|
2336
2402
|
});
|
|
2337
|
-
|
|
2403
|
+
const handle = async (route, startedAt) => {
|
|
2338
2404
|
const request = route.request();
|
|
2339
|
-
if (applied || !requestMatchesRoute(request.method(), request.url(), item.routeKey) || !shouldMutate(state, item)) {
|
|
2340
|
-
await route.fallback();
|
|
2341
|
-
return;
|
|
2342
|
-
}
|
|
2343
2405
|
if (item.level === "server") {
|
|
2344
2406
|
if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
|
|
2345
2407
|
await route.fallback();
|
|
@@ -2363,6 +2425,13 @@ async function installProbeInterception(page, item) {
|
|
|
2363
2425
|
return;
|
|
2364
2426
|
}
|
|
2365
2427
|
try {
|
|
2428
|
+
const action = faultRouteAction(item.fault);
|
|
2429
|
+
if (action) {
|
|
2430
|
+
applied = true;
|
|
2431
|
+
appliedFault = item.fault;
|
|
2432
|
+
await performRouteAction(route, action, startedAt);
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2366
2435
|
const response = await route.fetch();
|
|
2367
2436
|
const body = await response.text();
|
|
2368
2437
|
const out = computeFault(item.fault, {
|
|
@@ -2372,53 +2441,26 @@ async function installProbeInterception(page, item) {
|
|
|
2372
2441
|
});
|
|
2373
2442
|
applied = out.changed;
|
|
2374
2443
|
if (applied) appliedFault = item.fault;
|
|
2375
|
-
if (out.delayMs > 0) await new Promise((resolve2) => setTimeout(resolve2, out.delayMs));
|
|
2376
2444
|
const headers = fulfillHeaders(response.headers(), out.contentType);
|
|
2377
2445
|
await route.fulfill({ status: out.status, body: out.body, headers });
|
|
2378
2446
|
} catch {
|
|
2379
2447
|
applied = false;
|
|
2380
2448
|
await route.fallback();
|
|
2381
2449
|
}
|
|
2382
|
-
});
|
|
2383
|
-
return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
// src/internal/capture/inspect-on-failure.ts
|
|
2387
|
-
function isCi(ci) {
|
|
2388
|
-
return ci !== void 0 && ci !== "" && ci !== "false";
|
|
2389
|
-
}
|
|
2390
|
-
function shouldInspectOnFailure(gate) {
|
|
2391
|
-
if (gate.enabled !== "true") return false;
|
|
2392
|
-
if (isCi(gate.ci)) return false;
|
|
2393
|
-
if (gate.headless !== false) return false;
|
|
2394
|
-
if (gate.status !== "failed" && gate.status !== "timedOut") return false;
|
|
2395
|
-
if (gate.status === gate.expectedStatus) return false;
|
|
2396
|
-
return gate.retry >= gate.retries;
|
|
2397
|
-
}
|
|
2398
|
-
function environmentalSkipReason(gate) {
|
|
2399
|
-
if (gate.enabled !== "true") return null;
|
|
2400
|
-
if (gate.status !== "failed" && gate.status !== "timedOut") return null;
|
|
2401
|
-
if (gate.status === gate.expectedStatus) return null;
|
|
2402
|
-
if (isCi(gate.ci)) return "running under CI \u2014 this is a headed, local-only feature";
|
|
2403
|
-
if (gate.headless !== false) {
|
|
2404
|
-
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
2405
|
-
}
|
|
2406
|
-
return null;
|
|
2407
|
-
}
|
|
2408
|
-
function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT_ON_FAIL) {
|
|
2409
|
-
const use = testInfo.project?.use ?? {};
|
|
2410
|
-
return {
|
|
2411
|
-
enabled,
|
|
2412
|
-
ci: process.env.CI,
|
|
2413
|
-
status: testInfo.status,
|
|
2414
|
-
expectedStatus: testInfo.expectedStatus,
|
|
2415
|
-
headless: use.headless,
|
|
2416
|
-
retry: testInfo.retry,
|
|
2417
|
-
retries: testInfo.project?.retries ?? 0
|
|
2418
2450
|
};
|
|
2451
|
+
await installRouteRules(page, [
|
|
2452
|
+
{
|
|
2453
|
+
matches: (method, url) => requestMatchesRoute(method, url, item.routeKey),
|
|
2454
|
+
match: Math.max(1, item.nth ?? 1),
|
|
2455
|
+
handle,
|
|
2456
|
+
done: () => applied
|
|
2457
|
+
}
|
|
2458
|
+
]);
|
|
2459
|
+
return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
|
|
2419
2460
|
}
|
|
2420
2461
|
|
|
2421
|
-
// src/internal/
|
|
2462
|
+
// src/internal/flake/mode.ts
|
|
2463
|
+
var fs3 = __toESM(require("fs"));
|
|
2422
2464
|
var path4 = __toESM(require("path"));
|
|
2423
2465
|
|
|
2424
2466
|
// ../core/src/locator-chain.ts
|
|
@@ -2703,8 +2745,478 @@ function locatorCallValues(call, opts = {}) {
|
|
|
2703
2745
|
return call.args.map(value).filter((v) => v !== void 0);
|
|
2704
2746
|
}
|
|
2705
2747
|
|
|
2748
|
+
// ../core/src/error-parse.ts
|
|
2749
|
+
var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
|
|
2750
|
+
function stripAnsi(text) {
|
|
2751
|
+
return text.replace(ANSI_RE, "");
|
|
2752
|
+
}
|
|
2753
|
+
var SELECTOR_FN_RE = /\b(?:locator|frameLocator|getByRole|getByTestId|getByText|getByLabel|getByPlaceholder|getByAltText|getByTitle)\(/;
|
|
2754
|
+
var CHAIN_LINK_METHODS = /* @__PURE__ */ new Set([
|
|
2755
|
+
...LOCATOR_BUILDER_METHODS,
|
|
2756
|
+
"frameLocator",
|
|
2757
|
+
"contentFrame",
|
|
2758
|
+
"filter",
|
|
2759
|
+
"first",
|
|
2760
|
+
"last",
|
|
2761
|
+
"nth",
|
|
2762
|
+
"and",
|
|
2763
|
+
"or",
|
|
2764
|
+
"describe"
|
|
2765
|
+
]);
|
|
2766
|
+
function extractMessageHead(text) {
|
|
2767
|
+
let head = text;
|
|
2768
|
+
const callLogIdx = head.indexOf("\nCall log:");
|
|
2769
|
+
if (callLogIdx !== -1) head = head.slice(0, callLogIdx);
|
|
2770
|
+
const stackIdx = head.search(/\n\s+at /);
|
|
2771
|
+
if (stackIdx !== -1) head = head.slice(0, stackIdx);
|
|
2772
|
+
const lines = head.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
2773
|
+
return lines.slice(0, 5).join("\n");
|
|
2774
|
+
}
|
|
2775
|
+
function extractSelector(text) {
|
|
2776
|
+
const match = SELECTOR_FN_RE.exec(text);
|
|
2777
|
+
if (!match) return null;
|
|
2778
|
+
const start = match.index;
|
|
2779
|
+
let depth = 0;
|
|
2780
|
+
for (let i = start; i < Math.min(text.length, start + 200); i++) {
|
|
2781
|
+
const ch = text[i];
|
|
2782
|
+
if (ch === "(") {
|
|
2783
|
+
depth++;
|
|
2784
|
+
} else if (ch === ")") {
|
|
2785
|
+
depth--;
|
|
2786
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
2787
|
+
} else if (ch === "\n") {
|
|
2788
|
+
break;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
return text.slice(start, start + 80);
|
|
2792
|
+
}
|
|
2793
|
+
var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
|
|
2794
|
+
function extractTopFrame(text) {
|
|
2795
|
+
const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
|
|
2796
|
+
let m;
|
|
2797
|
+
while ((m = frameRe.exec(text)) !== null) {
|
|
2798
|
+
const file = m[1].replace(/\\/g, "/");
|
|
2799
|
+
if (file.includes("node_modules") || file.startsWith("node:")) continue;
|
|
2800
|
+
return { file, line: Number(m[2]), column: Number(m[3]) };
|
|
2801
|
+
}
|
|
2802
|
+
return null;
|
|
2803
|
+
}
|
|
2804
|
+
function extractTopFrameFile(text) {
|
|
2805
|
+
return extractTopFrame(text)?.file ?? null;
|
|
2806
|
+
}
|
|
2807
|
+
var SUBJECTS = "locator|page|frame|frameLocator|elementHandle|mouse|keyboard|touchscreen|browserContext|browser|apiRequestContext|request|response|route|download|dialog|fileChooser|expect|worker|jsHandle|clock|tracing|video";
|
|
2808
|
+
var CALL_RE = new RegExp(`\\b(${SUBJECTS})\\.(\\w+): `);
|
|
2809
|
+
|
|
2810
|
+
// ../core/src/error-signature.ts
|
|
2811
|
+
var UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
|
2812
|
+
var LONG_HEX_RE = /\b[0-9a-f]{8,}\b/gi;
|
|
2813
|
+
var SHORT_HEX_RE = /\b(?=[0-9a-f]*[a-f])(?=[0-9a-f]*[0-9])[0-9a-f]{6,7}\b/gi;
|
|
2814
|
+
var URL_RE = /\bhttps?:\/\/[^\s'"`)]+/gi;
|
|
2815
|
+
var EMAIL_RE = /\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b/gi;
|
|
2816
|
+
var SELECTOR_OPTION_RE = /\b(name|hasText|hasNotText|has|placeholder|label|title|alt|exact)\s*:\s*(['"`])(?:\\.|(?!\2)[\s\S])*?\2/gi;
|
|
2817
|
+
function classifyError(text) {
|
|
2818
|
+
if (/strict mode violation/i.test(text)) return "strict-mode";
|
|
2819
|
+
if (/\bexpect\(|\bexpect\.|Expected (?:string|substring|pattern|value)|\.toHave|\.toBe|\.toContain|\.toEqual/.test(text))
|
|
2820
|
+
return "assertion";
|
|
2821
|
+
if (/Target page, context or browser has been closed|Target closed|browser has been closed|Page crashed/i.test(text))
|
|
2822
|
+
return "crash";
|
|
2823
|
+
if (/net::ERR_|NS_ERROR_|Navigation failed/i.test(text)) return "navigation";
|
|
2824
|
+
if (/Timeout \d+m?s exceeded|TimeoutError|Timed out \d+m?s/i.test(text)) return "timeout";
|
|
2825
|
+
return "unknown";
|
|
2826
|
+
}
|
|
2827
|
+
function maskVolatile(text) {
|
|
2828
|
+
return text.replace(/^(\s*(?:Received|Expected)[^:\n]*:).*$/gm, "$1 <VALUE>").replace(URL_RE, "<URL>").replace(EMAIL_RE, "<EMAIL>").replace(UUID_RE, "<UUID>").replace(LONG_HEX_RE, "<HASH>").replace(SHORT_HEX_RE, "<HASH>").replace(/([A-Za-z])?(\d+)/g, (whole, letter) => letter ? whole : "<N>");
|
|
2829
|
+
}
|
|
2830
|
+
function maskSelectorOptions(text) {
|
|
2831
|
+
return text.replace(SELECTOR_OPTION_RE, (_m, key) => `${key}: <STR>`);
|
|
2832
|
+
}
|
|
2833
|
+
function extractErrorSignature(rawError) {
|
|
2834
|
+
const text = stripAnsi(rawError);
|
|
2835
|
+
const errorType = classifyError(text);
|
|
2836
|
+
const normalizedMessage = maskVolatile(maskSelectorOptions(extractMessageHead(text)));
|
|
2837
|
+
const selector = extractSelector(text);
|
|
2838
|
+
const topFrameFile = extractTopFrameFile(text);
|
|
2839
|
+
const signature = (normalizedMessage.split("\n")[0] || "").slice(0, 200) || "Unknown error";
|
|
2840
|
+
return { errorType, signature, normalizedMessage, selector, topFrameFile };
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
// ../core/src/flake-plan.ts
|
|
2844
|
+
var FLAKE_PLAN_VERSION = 1;
|
|
2845
|
+
var FLAKE_CONDITION_KINDS = [
|
|
2846
|
+
"delay",
|
|
2847
|
+
"fail",
|
|
2848
|
+
"cpu",
|
|
2849
|
+
"network",
|
|
2850
|
+
"alongside",
|
|
2851
|
+
"after",
|
|
2852
|
+
"project"
|
|
2853
|
+
];
|
|
2854
|
+
var COMMAND_CONDITION_KINDS = ["alongside", "after", "project"];
|
|
2855
|
+
function flakeErrorSignature(rawError) {
|
|
2856
|
+
return extractErrorSignature(rawError).normalizedMessage;
|
|
2857
|
+
}
|
|
2858
|
+
var FlakePlanError = class extends Error {
|
|
2859
|
+
constructor(message) {
|
|
2860
|
+
super(`Invalid flake plan: ${message}`);
|
|
2861
|
+
this.name = "FlakePlanError";
|
|
2862
|
+
}
|
|
2863
|
+
};
|
|
2864
|
+
function isObject(value) {
|
|
2865
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2866
|
+
}
|
|
2867
|
+
function str(obj, key, at) {
|
|
2868
|
+
const value = obj[key];
|
|
2869
|
+
if (typeof value !== "string" || !value.trim()) throw new FlakePlanError(`${at}.${key} must be a non-empty string`);
|
|
2870
|
+
return value;
|
|
2871
|
+
}
|
|
2872
|
+
function num(obj, key, at, min, max = Number.MAX_SAFE_INTEGER) {
|
|
2873
|
+
const value = obj[key];
|
|
2874
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
|
|
2875
|
+
throw new FlakePlanError(`${at}.${key} must be a number from ${min} to ${max}`);
|
|
2876
|
+
}
|
|
2877
|
+
return value;
|
|
2878
|
+
}
|
|
2879
|
+
function stringList(value, at) {
|
|
2880
|
+
if (value === void 0) return [];
|
|
2881
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
2882
|
+
throw new FlakePlanError(`${at} must be a list of strings`);
|
|
2883
|
+
}
|
|
2884
|
+
return value;
|
|
2885
|
+
}
|
|
2886
|
+
var ROUTE_KEY_RE = /^[A-Z]+ \/\S*$/;
|
|
2887
|
+
function routeKey(obj, at) {
|
|
2888
|
+
const route = str(obj, "route", at);
|
|
2889
|
+
if (!ROUTE_KEY_RE.test(route)) {
|
|
2890
|
+
throw new FlakePlanError(`${at}.route must be a method and a path, like "GET /api/cart/:id" (got "${route}")`);
|
|
2891
|
+
}
|
|
2892
|
+
return route;
|
|
2893
|
+
}
|
|
2894
|
+
function routeMatch(obj, at) {
|
|
2895
|
+
const value = obj.match;
|
|
2896
|
+
if (value === void 0 || value === "all") return "all";
|
|
2897
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
|
|
2898
|
+
throw new FlakePlanError(`${at}.match must be "all" or a whole number from 1`);
|
|
2899
|
+
}
|
|
2900
|
+
function testRef(value, at) {
|
|
2901
|
+
if (!isObject(value)) throw new FlakePlanError(`${at} must be an object`);
|
|
2902
|
+
return {
|
|
2903
|
+
file: str(value, "file", at),
|
|
2904
|
+
title: str(value, "title", at),
|
|
2905
|
+
suite: stringList(value.suite, `${at}.suite`)
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
function parseCondition(value, at) {
|
|
2909
|
+
if (!isObject(value)) throw new FlakePlanError(`${at} must be an object`);
|
|
2910
|
+
const kind = value.kind;
|
|
2911
|
+
switch (kind) {
|
|
2912
|
+
case "delay":
|
|
2913
|
+
return { kind, route: routeKey(value, at), ms: num(value, "ms", at, 0, 6e5), match: routeMatch(value, at) };
|
|
2914
|
+
case "fail": {
|
|
2915
|
+
const route = routeKey(value, at);
|
|
2916
|
+
const match = routeMatch(value, at);
|
|
2917
|
+
if (value.abort === true) {
|
|
2918
|
+
if (value.status !== void 0) throw new FlakePlanError(`${at} sets both status and abort; choose one`);
|
|
2919
|
+
return { kind, route, abort: true, match };
|
|
2920
|
+
}
|
|
2921
|
+
const status = num(value, "status", at, 100, 599);
|
|
2922
|
+
if (!Number.isInteger(status)) throw new FlakePlanError(`${at}.status must be a whole number`);
|
|
2923
|
+
return { kind, route, status, match };
|
|
2924
|
+
}
|
|
2925
|
+
case "cpu":
|
|
2926
|
+
return { kind, rate: num(value, "rate", at, 1, 100) };
|
|
2927
|
+
case "network":
|
|
2928
|
+
return {
|
|
2929
|
+
kind,
|
|
2930
|
+
latencyMs: num(value, "latencyMs", at, 0, 6e4),
|
|
2931
|
+
downKbps: num(value, "downKbps", at, 1),
|
|
2932
|
+
upKbps: num(value, "upKbps", at, 1)
|
|
2933
|
+
};
|
|
2934
|
+
case "alongside":
|
|
2935
|
+
case "after":
|
|
2936
|
+
return { kind, test: testRef(value.test, `${at}.test`) };
|
|
2937
|
+
case "project":
|
|
2938
|
+
return { kind, name: str(value, "name", at) };
|
|
2939
|
+
default:
|
|
2940
|
+
throw new FlakePlanError(
|
|
2941
|
+
`${at}.kind ${JSON.stringify(kind)} is not a condition this version knows (${FLAKE_CONDITION_KINDS.join(", ")})`
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
function parseFlakePlan(value) {
|
|
2946
|
+
if (!isObject(value)) throw new FlakePlanError("the plan must be a JSON object");
|
|
2947
|
+
if (value.version !== FLAKE_PLAN_VERSION) {
|
|
2948
|
+
throw new FlakePlanError(`version must be ${FLAKE_PLAN_VERSION} (got ${JSON.stringify(value.version)})`);
|
|
2949
|
+
}
|
|
2950
|
+
const experimentId = str(value, "experimentId", "plan");
|
|
2951
|
+
if (!isObject(value.test)) throw new FlakePlanError("plan.test must be an object");
|
|
2952
|
+
const ref = testRef(value.test, "plan.test");
|
|
2953
|
+
const project = value.test.project;
|
|
2954
|
+
if (project !== void 0 && project !== null && (typeof project !== "string" || !project.trim())) {
|
|
2955
|
+
throw new FlakePlanError("plan.test.project must be a non-empty string or null");
|
|
2956
|
+
}
|
|
2957
|
+
if (!isObject(value.arm)) throw new FlakePlanError("plan.arm must be an object");
|
|
2958
|
+
const armId = str(value.arm, "id", "plan.arm");
|
|
2959
|
+
const rawConditions = value.arm.conditions ?? [];
|
|
2960
|
+
if (!Array.isArray(rawConditions)) throw new FlakePlanError("plan.arm.conditions must be a list");
|
|
2961
|
+
const conditions = rawConditions.map((c, i) => parseCondition(c, `plan.arm.conditions[${i}]`));
|
|
2962
|
+
return {
|
|
2963
|
+
version: FLAKE_PLAN_VERSION,
|
|
2964
|
+
experimentId,
|
|
2965
|
+
test: { ...ref, project: typeof project === "string" ? project : null },
|
|
2966
|
+
arm: { id: armId, conditions },
|
|
2967
|
+
errorSignatures: stringList(value.errorSignatures, "plan.errorSignatures")
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
function parseFlakePlanText(text) {
|
|
2971
|
+
let json;
|
|
2972
|
+
try {
|
|
2973
|
+
json = JSON.parse(text);
|
|
2974
|
+
} catch (error) {
|
|
2975
|
+
throw new FlakePlanError(`not valid JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
2976
|
+
}
|
|
2977
|
+
return parseFlakePlan(json);
|
|
2978
|
+
}
|
|
2979
|
+
function suiteEqual(a, b) {
|
|
2980
|
+
return !!b && a.length === b.length && a.every((title, i) => title === b[i]);
|
|
2981
|
+
}
|
|
2982
|
+
function flakeTestMatches(ref, test) {
|
|
2983
|
+
if (!test.file || ref.file !== test.file || ref.title !== test.title) return false;
|
|
2984
|
+
return test.suite === void 0 || ref.suite.length === 0 || suiteEqual(ref.suite, test.suite);
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// src/internal/support/errors.ts
|
|
2988
|
+
function errorMessage(error) {
|
|
2989
|
+
if (error instanceof Error) return error.message;
|
|
2990
|
+
return String(error);
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
// src/internal/flake/mode.ts
|
|
2994
|
+
function isFlakeMode() {
|
|
2995
|
+
return !!process.env[PIWI_FLAKE_ENV.plan]?.trim();
|
|
2996
|
+
}
|
|
2997
|
+
function labModeConflict() {
|
|
2998
|
+
if (!isProbeMode() || !isFlakeMode()) return null;
|
|
2999
|
+
return `[Piwi Dashboard] ${PIWI_PROBE_ENV.flag} and ${PIWI_FLAKE_ENV.plan} are both set: a run is a probe run or a flake-lab run, not both. Unset one of them.`;
|
|
3000
|
+
}
|
|
3001
|
+
var planCache2 = null;
|
|
3002
|
+
function loadFlakePlan() {
|
|
3003
|
+
const file = process.env[PIWI_FLAKE_ENV.plan]?.trim() ?? "";
|
|
3004
|
+
if (planCache2 && planCache2.file === file) return planCache2.plan;
|
|
3005
|
+
let text;
|
|
3006
|
+
try {
|
|
3007
|
+
text = fs3.readFileSync(file, "utf8");
|
|
3008
|
+
} catch (error) {
|
|
3009
|
+
throw new Error(`[Piwi Dashboard] Cannot read the flake plan ${file}: ${errorMessage(error)}`);
|
|
3010
|
+
}
|
|
3011
|
+
try {
|
|
3012
|
+
const plan = parseFlakePlanText(text);
|
|
3013
|
+
planCache2 = { file, plan };
|
|
3014
|
+
return plan;
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
throw new Error(`[Piwi Dashboard] ${errorMessage(error)} (${file})`);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
function relativeTestFile(file) {
|
|
3020
|
+
return file ? path4.relative(process.cwd(), file).split(path4.sep).join("/") : null;
|
|
3021
|
+
}
|
|
3022
|
+
function flakeRoleForTest(plan, test) {
|
|
3023
|
+
const running = {
|
|
3024
|
+
file: relativeTestFile(test.file),
|
|
3025
|
+
title: test.title,
|
|
3026
|
+
suite: test.titlePath ? test.titlePath.slice(1, -1) : void 0
|
|
3027
|
+
};
|
|
3028
|
+
if (flakeTestMatches(plan.test, running) && (plan.test.project === null || plan.test.project === test.project)) {
|
|
3029
|
+
return "target";
|
|
3030
|
+
}
|
|
3031
|
+
const companion = plan.arm.conditions.some(
|
|
3032
|
+
(c) => (c.kind === "alongside" || c.kind === "after") && flakeTestMatches(c.test, running)
|
|
3033
|
+
);
|
|
3034
|
+
return companion ? "companion" : null;
|
|
3035
|
+
}
|
|
3036
|
+
function pageBrowserName(page) {
|
|
3037
|
+
try {
|
|
3038
|
+
return page.context().browser()?.browserType().name() ?? null;
|
|
3039
|
+
} catch {
|
|
3040
|
+
return null;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
function cdpCommandFor(condition) {
|
|
3044
|
+
if (condition.kind === "cpu") return { method: "Emulation.setCPUThrottlingRate", params: { rate: condition.rate } };
|
|
3045
|
+
if (condition.kind === "network") {
|
|
3046
|
+
return {
|
|
3047
|
+
method: "Network.emulateNetworkConditions",
|
|
3048
|
+
params: {
|
|
3049
|
+
offline: false,
|
|
3050
|
+
latency: condition.latencyMs,
|
|
3051
|
+
// Kilobits per second to bytes per second.
|
|
3052
|
+
downloadThroughput: condition.downKbps * 1e3 / 8,
|
|
3053
|
+
uploadThroughput: condition.upKbps * 1e3 / 8
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
return null;
|
|
3058
|
+
}
|
|
3059
|
+
function conditionRouteAction(condition) {
|
|
3060
|
+
if (condition.kind === "delay") return { kind: "delay", ms: condition.ms };
|
|
3061
|
+
if (condition.kind === "fail")
|
|
3062
|
+
return "abort" in condition ? { kind: "abort" } : { kind: "status", status: condition.status };
|
|
3063
|
+
return null;
|
|
3064
|
+
}
|
|
3065
|
+
function unappliedReports(plan) {
|
|
3066
|
+
return plan.arm.conditions.map((c) => {
|
|
3067
|
+
if (COMMAND_CONDITION_KINDS.includes(c.kind)) return { kind: c.kind, outcome: "by-command" };
|
|
3068
|
+
if (cdpCommandFor(c)) return { kind: c.kind, outcome: "skipped", note: "the attempt opened no page" };
|
|
3069
|
+
return { kind: c.kind, outcome: "not-matched" };
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
async function installFlakeConditions(page, plan) {
|
|
3073
|
+
const conditions = plan.arm.conditions;
|
|
3074
|
+
const reports = unappliedReports(plan);
|
|
3075
|
+
const rules = [];
|
|
3076
|
+
conditions.forEach((condition, i) => {
|
|
3077
|
+
const action = conditionRouteAction(condition);
|
|
3078
|
+
if (!action || condition.kind !== "delay" && condition.kind !== "fail") return;
|
|
3079
|
+
const route = condition.route;
|
|
3080
|
+
rules.push({
|
|
3081
|
+
matches: (method, url) => requestRouteKey(method, url) === route,
|
|
3082
|
+
match: condition.match,
|
|
3083
|
+
handle: async (r, startedAt) => {
|
|
3084
|
+
reports[i] = { kind: condition.kind, outcome: "applied" };
|
|
3085
|
+
try {
|
|
3086
|
+
await performRouteAction(r, action, startedAt);
|
|
3087
|
+
} catch {
|
|
3088
|
+
await r.fallback().catch(() => {
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
3093
|
+
});
|
|
3094
|
+
await installRouteRules(page, rules);
|
|
3095
|
+
const cdp = conditions.map((c, i) => ({ i, command: cdpCommandFor(c) })).filter((c) => c.command);
|
|
3096
|
+
if (cdp.length > 0) {
|
|
3097
|
+
const browserName = pageBrowserName(page);
|
|
3098
|
+
if (browserName !== null && browserName !== "chromium") {
|
|
3099
|
+
for (const { i } of cdp) {
|
|
3100
|
+
reports[i] = {
|
|
3101
|
+
kind: conditions[i].kind,
|
|
3102
|
+
outcome: "skipped",
|
|
3103
|
+
note: `needs Chromium; this attempt ran in ${browserName}`
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
} else {
|
|
3107
|
+
try {
|
|
3108
|
+
const session = await page.context().newCDPSession(page);
|
|
3109
|
+
for (const { i, command } of cdp) {
|
|
3110
|
+
try {
|
|
3111
|
+
if (command.method.startsWith("Network.")) await session.send("Network.enable");
|
|
3112
|
+
await session.send(command.method, command.params);
|
|
3113
|
+
reports[i] = { kind: conditions[i].kind, outcome: "applied" };
|
|
3114
|
+
} catch (error) {
|
|
3115
|
+
reports[i] = { kind: conditions[i].kind, outcome: "skipped", note: errorMessage(error) };
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
} catch (error) {
|
|
3119
|
+
for (const { i } of cdp) {
|
|
3120
|
+
reports[i] = {
|
|
3121
|
+
kind: conditions[i].kind,
|
|
3122
|
+
outcome: "skipped",
|
|
3123
|
+
note: `no DevTools session: ${errorMessage(error)}`
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
return { reports: () => reports.map((r) => ({ ...r })) };
|
|
3130
|
+
}
|
|
3131
|
+
var FAILED_STATUSES = /* @__PURE__ */ new Set(["failed", "timedOut", "interrupted"]);
|
|
3132
|
+
function flakeResultLine(plan, attempt) {
|
|
3133
|
+
const failed = FAILED_STATUSES.has(attempt.status);
|
|
3134
|
+
const errorSignature = failed && attempt.errorText ? flakeErrorSignature(attempt.errorText) : null;
|
|
3135
|
+
return {
|
|
3136
|
+
version: FLAKE_PLAN_VERSION,
|
|
3137
|
+
experimentId: plan.experimentId,
|
|
3138
|
+
armId: plan.arm.id,
|
|
3139
|
+
role: attempt.role,
|
|
3140
|
+
file: relativeTestFile(attempt.file),
|
|
3141
|
+
title: attempt.title,
|
|
3142
|
+
project: attempt.project,
|
|
3143
|
+
browserName: attempt.browserName,
|
|
3144
|
+
status: attempt.status,
|
|
3145
|
+
errorSignature,
|
|
3146
|
+
matchesHistory: errorSignature !== null && plan.errorSignatures.includes(errorSignature),
|
|
3147
|
+
startedAt: attempt.startedAt,
|
|
3148
|
+
duration: attempt.duration,
|
|
3149
|
+
workerIndex: attempt.workerIndex,
|
|
3150
|
+
parallelIndex: attempt.parallelIndex,
|
|
3151
|
+
repeatEachIndex: attempt.repeatEachIndex,
|
|
3152
|
+
retry: attempt.retry,
|
|
3153
|
+
conditions: attempt.role === "target" ? attempt.conditions : []
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3156
|
+
function recordFlakeResult(line) {
|
|
3157
|
+
const file = process.env[PIWI_FLAKE_ENV.results]?.trim();
|
|
3158
|
+
if (!file) return;
|
|
3159
|
+
try {
|
|
3160
|
+
fs3.appendFileSync(file, `${JSON.stringify(line)}
|
|
3161
|
+
`);
|
|
3162
|
+
} catch {
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// ../core/src/error-text.ts
|
|
3167
|
+
function joinErrorMessages(errors) {
|
|
3168
|
+
if (!errors || errors.length === 0) return null;
|
|
3169
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3170
|
+
const parts = [];
|
|
3171
|
+
for (const err of errors) {
|
|
3172
|
+
const msg = err.message ?? "";
|
|
3173
|
+
if (!msg) continue;
|
|
3174
|
+
const dedupeKey = msg.trim().slice(0, 200);
|
|
3175
|
+
if (seen.has(dedupeKey)) continue;
|
|
3176
|
+
seen.add(dedupeKey);
|
|
3177
|
+
parts.push(msg);
|
|
3178
|
+
}
|
|
3179
|
+
return parts.join("\n---\n") || null;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// src/internal/capture/inspect-on-failure.ts
|
|
3183
|
+
function isCi(ci) {
|
|
3184
|
+
return ci !== void 0 && ci !== "" && ci !== "false";
|
|
3185
|
+
}
|
|
3186
|
+
function shouldInspectOnFailure(gate) {
|
|
3187
|
+
if (gate.enabled !== "true") return false;
|
|
3188
|
+
if (isCi(gate.ci)) return false;
|
|
3189
|
+
if (gate.headless !== false) return false;
|
|
3190
|
+
if (gate.status !== "failed" && gate.status !== "timedOut") return false;
|
|
3191
|
+
if (gate.status === gate.expectedStatus) return false;
|
|
3192
|
+
return gate.retry >= gate.retries;
|
|
3193
|
+
}
|
|
3194
|
+
function environmentalSkipReason(gate) {
|
|
3195
|
+
if (gate.enabled !== "true") return null;
|
|
3196
|
+
if (gate.status !== "failed" && gate.status !== "timedOut") return null;
|
|
3197
|
+
if (gate.status === gate.expectedStatus) return null;
|
|
3198
|
+
if (isCi(gate.ci)) return "running under CI \u2014 this is a headed, local-only feature";
|
|
3199
|
+
if (gate.headless !== false) {
|
|
3200
|
+
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
3201
|
+
}
|
|
3202
|
+
return null;
|
|
3203
|
+
}
|
|
3204
|
+
function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT_ON_FAIL) {
|
|
3205
|
+
const use = testInfo.project?.use ?? {};
|
|
3206
|
+
return {
|
|
3207
|
+
enabled,
|
|
3208
|
+
ci: process.env.CI,
|
|
3209
|
+
status: testInfo.status,
|
|
3210
|
+
expectedStatus: testInfo.expectedStatus,
|
|
3211
|
+
headless: use.headless,
|
|
3212
|
+
retry: testInfo.retry,
|
|
3213
|
+
retries: testInfo.project?.retries ?? 0
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
|
|
2706
3217
|
// src/internal/capture/pick-on-failure.ts
|
|
2707
|
-
var
|
|
3218
|
+
var path5 = __toESM(require("path"));
|
|
3219
|
+
var ANSI_RE2 = /\[[0-9;]*m/g;
|
|
2708
3220
|
function parseLeafLocatorExpression(rawExpr) {
|
|
2709
3221
|
const leaf = parseLeafLocatorCall(rawExpr);
|
|
2710
3222
|
return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
|
|
@@ -2714,13 +3226,13 @@ function deriveFailedLocator(testInfo) {
|
|
|
2714
3226
|
const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
|
|
2715
3227
|
for (const err of errors) {
|
|
2716
3228
|
const text = `${err.message ?? ""}
|
|
2717
|
-
${err.stack ?? ""}`.replace(
|
|
3229
|
+
${err.stack ?? ""}`.replace(ANSI_RE2, "");
|
|
2718
3230
|
const line = /^\s*Locator:\s*(.+)$/m.exec(text);
|
|
2719
3231
|
if (!line) continue;
|
|
2720
3232
|
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
2721
3233
|
if (!parsed) continue;
|
|
2722
3234
|
const loc = err.location;
|
|
2723
|
-
const location = loc ? `${
|
|
3235
|
+
const location = loc ? `${path5.relative(process.cwd(), loc.file).split(path5.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
2724
3236
|
return { method: parsed.method, args: parsed.args, location };
|
|
2725
3237
|
}
|
|
2726
3238
|
return null;
|
|
@@ -2869,9 +3381,9 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
2869
3381
|
}
|
|
2870
3382
|
|
|
2871
3383
|
// src/internal/support/aria-sampling.ts
|
|
2872
|
-
var
|
|
3384
|
+
var path6 = __toESM(require("path"));
|
|
2873
3385
|
var os3 = __toESM(require("os"));
|
|
2874
|
-
var
|
|
3386
|
+
var fs4 = __toESM(require("fs"));
|
|
2875
3387
|
|
|
2876
3388
|
// src/internal/support/instance-id.ts
|
|
2877
3389
|
var crypto = __toESM(require("crypto"));
|
|
@@ -2885,14 +3397,14 @@ function ariaSampleIdentity(filePath, title) {
|
|
|
2885
3397
|
return `${filePath}\0${title}`;
|
|
2886
3398
|
}
|
|
2887
3399
|
function getAriaSampleFilePath(projectName) {
|
|
2888
|
-
return
|
|
3400
|
+
return path6.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
|
|
2889
3401
|
}
|
|
2890
3402
|
var cachedSets = /* @__PURE__ */ new Map();
|
|
2891
3403
|
function loadAriaSampleSet(projectName) {
|
|
2892
3404
|
if (cachedSets.has(projectName)) return cachedSets.get(projectName);
|
|
2893
3405
|
let set = null;
|
|
2894
3406
|
try {
|
|
2895
|
-
const raw =
|
|
3407
|
+
const raw = fs4.readFileSync(getAriaSampleFilePath(projectName), "utf8");
|
|
2896
3408
|
const parsed = JSON.parse(raw);
|
|
2897
3409
|
if (parsed.projectName === projectName && Array.isArray(parsed.identities)) {
|
|
2898
3410
|
set = new Set(parsed.identities.filter((x) => typeof x === "string"));
|
|
@@ -2903,33 +3415,33 @@ function loadAriaSampleSet(projectName) {
|
|
|
2903
3415
|
cachedSets.set(projectName, set);
|
|
2904
3416
|
return set;
|
|
2905
3417
|
}
|
|
2906
|
-
function
|
|
2907
|
-
return
|
|
3418
|
+
function relativeTestFile2(file) {
|
|
3419
|
+
return path6.relative(process.cwd(), file).split(path6.sep).join("/");
|
|
2908
3420
|
}
|
|
2909
3421
|
function isDueForAriaSample(testInfo) {
|
|
2910
3422
|
const projectName = process.env.PIWI_PROJECT_NAME;
|
|
2911
3423
|
if (!projectName) return false;
|
|
2912
3424
|
const set = loadAriaSampleSet(projectName);
|
|
2913
3425
|
if (!set || set.size === 0) return false;
|
|
2914
|
-
return set.has(ariaSampleIdentity(
|
|
3426
|
+
return set.has(ariaSampleIdentity(relativeTestFile2(testInfo.file), testInfo.title));
|
|
2915
3427
|
}
|
|
2916
3428
|
|
|
2917
3429
|
// src/internal/capture/quiet-capture.ts
|
|
2918
3430
|
var import_node_module = require("module");
|
|
2919
|
-
var
|
|
3431
|
+
var path8 = __toESM(require("path"));
|
|
2920
3432
|
|
|
2921
3433
|
// src/internal/support/reporter-version.ts
|
|
2922
|
-
var
|
|
2923
|
-
var
|
|
3434
|
+
var fs5 = __toESM(require("fs"));
|
|
3435
|
+
var path7 = __toESM(require("path"));
|
|
2924
3436
|
var CANDIDATE_OFFSETS = ["..", "../../.."];
|
|
2925
3437
|
function findOwnPackageJson(fromDir) {
|
|
2926
3438
|
for (const offset of CANDIDATE_OFFSETS) {
|
|
2927
3439
|
try {
|
|
2928
|
-
const pkgPath =
|
|
2929
|
-
const pkg = JSON.parse(
|
|
3440
|
+
const pkgPath = path7.resolve(fromDir, offset, "package.json");
|
|
3441
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
2930
3442
|
const { name, version } = pkg;
|
|
2931
3443
|
if (name === "@piwitests/reporter" && typeof version === "string")
|
|
2932
|
-
return { version, root:
|
|
3444
|
+
return { version, root: path7.dirname(pkgPath) };
|
|
2933
3445
|
} catch {
|
|
2934
3446
|
}
|
|
2935
3447
|
}
|
|
@@ -2962,7 +3474,7 @@ function resolveBoxing() {
|
|
|
2962
3474
|
if (typeof set !== "function") continue;
|
|
2963
3475
|
return {
|
|
2964
3476
|
set,
|
|
2965
|
-
prefixes: [
|
|
3477
|
+
prefixes: [path8.dirname(playwrightPackageJson), path8.join(root, "dist") + path8.sep]
|
|
2966
3478
|
};
|
|
2967
3479
|
}
|
|
2968
3480
|
} catch {
|
|
@@ -2987,6 +3499,443 @@ function internalCall(owner, run) {
|
|
|
2987
3499
|
return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
|
|
2988
3500
|
}
|
|
2989
3501
|
|
|
3502
|
+
// src/internal/capture/code-reach.ts
|
|
3503
|
+
var crypto2 = __toESM(require("crypto"));
|
|
3504
|
+
var fs6 = __toESM(require("fs"));
|
|
3505
|
+
var path9 = __toESM(require("path"));
|
|
3506
|
+
var import_node_child_process = require("child_process");
|
|
3507
|
+
|
|
3508
|
+
// ../core/src/source-map.ts
|
|
3509
|
+
var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
3510
|
+
var BASE64_VALUE = new Int8Array(128).fill(-1);
|
|
3511
|
+
for (let i = 0; i < BASE64.length; i++) BASE64_VALUE[BASE64.charCodeAt(i)] = i;
|
|
3512
|
+
function decodeMappings(mappings, sourceBase) {
|
|
3513
|
+
const lines = [];
|
|
3514
|
+
let current = { columns: [], sources: [] };
|
|
3515
|
+
let column = 0;
|
|
3516
|
+
let source = 0;
|
|
3517
|
+
const fields = [];
|
|
3518
|
+
let value = 0;
|
|
3519
|
+
let shift = 0;
|
|
3520
|
+
const flush = () => {
|
|
3521
|
+
if (fields.length === 0) return;
|
|
3522
|
+
column += fields[0];
|
|
3523
|
+
if (fields.length >= 4) {
|
|
3524
|
+
source += fields[1];
|
|
3525
|
+
current.columns.push(column);
|
|
3526
|
+
current.sources.push(sourceBase + source);
|
|
3527
|
+
} else {
|
|
3528
|
+
current.columns.push(column);
|
|
3529
|
+
current.sources.push(-1);
|
|
3530
|
+
}
|
|
3531
|
+
fields.length = 0;
|
|
3532
|
+
};
|
|
3533
|
+
for (let i = 0; i <= mappings.length; i++) {
|
|
3534
|
+
const ch = i < mappings.length ? mappings.charCodeAt(i) : 59;
|
|
3535
|
+
if (ch === 59 || ch === 44) {
|
|
3536
|
+
flush();
|
|
3537
|
+
if (ch === 59) {
|
|
3538
|
+
lines.push(current);
|
|
3539
|
+
current = { columns: [], sources: [] };
|
|
3540
|
+
column = 0;
|
|
3541
|
+
}
|
|
3542
|
+
continue;
|
|
3543
|
+
}
|
|
3544
|
+
const digit = ch < 128 ? BASE64_VALUE[ch] : -1;
|
|
3545
|
+
if (digit < 0) continue;
|
|
3546
|
+
value += (digit & 31) << shift;
|
|
3547
|
+
if (digit & 32) {
|
|
3548
|
+
shift += 5;
|
|
3549
|
+
continue;
|
|
3550
|
+
}
|
|
3551
|
+
fields.push(value & 1 ? -(value >>> 1) : value >>> 1);
|
|
3552
|
+
value = 0;
|
|
3553
|
+
shift = 0;
|
|
3554
|
+
}
|
|
3555
|
+
for (const line of lines) {
|
|
3556
|
+
if (line.columns.every((c, i) => i === 0 || c >= line.columns[i - 1])) continue;
|
|
3557
|
+
const order = line.columns.map((_, i) => i).sort((a, b) => line.columns[a] - line.columns[b]);
|
|
3558
|
+
line.columns = order.map((i) => line.columns[i]);
|
|
3559
|
+
line.sources = order.map((i) => line.sources[i]);
|
|
3560
|
+
}
|
|
3561
|
+
return lines;
|
|
3562
|
+
}
|
|
3563
|
+
function joinRoot(root, source) {
|
|
3564
|
+
if (!root || /^[a-z][\w+.-]*:/i.test(source) || source.startsWith("/")) return source;
|
|
3565
|
+
return root.endsWith("/") ? root + source : `${root}/${source}`;
|
|
3566
|
+
}
|
|
3567
|
+
function decodeSourceMap(raw) {
|
|
3568
|
+
const sources = [];
|
|
3569
|
+
const lines = [];
|
|
3570
|
+
const add = (map, lineOffset, columnOffset) => {
|
|
3571
|
+
if (map.sections) {
|
|
3572
|
+
for (const section of map.sections) {
|
|
3573
|
+
if (!add(section.map, section.offset.line, section.offset.column)) return false;
|
|
3574
|
+
}
|
|
3575
|
+
return true;
|
|
3576
|
+
}
|
|
3577
|
+
if (typeof map.mappings !== "string" || !Array.isArray(map.sources)) return false;
|
|
3578
|
+
const base = sources.length;
|
|
3579
|
+
for (const s of map.sources) sources.push(joinRoot(map.sourceRoot, s ?? ""));
|
|
3580
|
+
decodeMappings(map.mappings, base).forEach((segments, i) => {
|
|
3581
|
+
const at = lineOffset + i;
|
|
3582
|
+
const shifted = i === 0 && columnOffset ? segments.columns.map((c) => c + columnOffset) : segments.columns;
|
|
3583
|
+
const existing = lines[at];
|
|
3584
|
+
if (!existing) lines[at] = { columns: shifted, sources: segments.sources };
|
|
3585
|
+
else {
|
|
3586
|
+
existing.columns.push(...shifted);
|
|
3587
|
+
existing.sources.push(...segments.sources);
|
|
3588
|
+
}
|
|
3589
|
+
});
|
|
3590
|
+
return true;
|
|
3591
|
+
};
|
|
3592
|
+
if (raw.version !== void 0 && raw.version !== 3) return null;
|
|
3593
|
+
if (!add(raw, 0, 0)) return null;
|
|
3594
|
+
return {
|
|
3595
|
+
sources,
|
|
3596
|
+
sourceAt(line, column) {
|
|
3597
|
+
const segments = lines[line];
|
|
3598
|
+
if (!segments || segments.columns.length === 0) return null;
|
|
3599
|
+
let lo = 0;
|
|
3600
|
+
let hi = segments.columns.length - 1;
|
|
3601
|
+
if (segments.columns[0] > column) return null;
|
|
3602
|
+
while (lo < hi) {
|
|
3603
|
+
const mid = lo + hi + 1 >> 1;
|
|
3604
|
+
if (segments.columns[mid] <= column) lo = mid;
|
|
3605
|
+
else hi = mid - 1;
|
|
3606
|
+
}
|
|
3607
|
+
const source = segments.sources[lo];
|
|
3608
|
+
return source < 0 ? null : source;
|
|
3609
|
+
},
|
|
3610
|
+
mapsBetween(source, start, end) {
|
|
3611
|
+
for (let line = start.line; line <= end.line && line < lines.length; line++) {
|
|
3612
|
+
const segments = lines[line];
|
|
3613
|
+
if (!segments) continue;
|
|
3614
|
+
let i = 0;
|
|
3615
|
+
if (line === start.line) {
|
|
3616
|
+
let hi = segments.columns.length;
|
|
3617
|
+
while (i < hi) {
|
|
3618
|
+
const mid = i + hi >> 1;
|
|
3619
|
+
if (segments.columns[mid] < start.column) i = mid + 1;
|
|
3620
|
+
else hi = mid;
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
for (; i < segments.columns.length; i++) {
|
|
3624
|
+
if (line === end.line && segments.columns[i] >= end.column) return false;
|
|
3625
|
+
if (segments.sources[i] === source) return true;
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
return false;
|
|
3629
|
+
}
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
function lineStarts(text) {
|
|
3633
|
+
const starts = [0];
|
|
3634
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
|
|
3635
|
+
return starts;
|
|
3636
|
+
}
|
|
3637
|
+
function offsetToPosition(starts, offset) {
|
|
3638
|
+
let lo = 0;
|
|
3639
|
+
let hi = starts.length - 1;
|
|
3640
|
+
while (lo < hi) {
|
|
3641
|
+
const mid = lo + hi + 1 >> 1;
|
|
3642
|
+
if (starts[mid] <= offset) lo = mid;
|
|
3643
|
+
else hi = mid - 1;
|
|
3644
|
+
}
|
|
3645
|
+
return { line: lo, column: offset - starts[lo] };
|
|
3646
|
+
}
|
|
3647
|
+
function sourceMappingUrl(script) {
|
|
3648
|
+
const tail = script.slice(-4096);
|
|
3649
|
+
const matches = [...tail.matchAll(/(?:\/\/|\/\*)[#@]\s*sourceMappingURL=([^\s*'"]+)/g)];
|
|
3650
|
+
return matches.length ? matches[matches.length - 1][1] : null;
|
|
3651
|
+
}
|
|
3652
|
+
function decodeDataUrl(url) {
|
|
3653
|
+
const m = /^data:([^,]*?)(;base64)?,(.*)$/s.exec(url);
|
|
3654
|
+
if (!m) return null;
|
|
3655
|
+
try {
|
|
3656
|
+
if (!m[2]) return decodeURIComponent(m[3]);
|
|
3657
|
+
const binary = globalThis.atob(m[3]);
|
|
3658
|
+
const bytes = new Uint8Array(binary.length);
|
|
3659
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
3660
|
+
return new TextDecoder().decode(bytes);
|
|
3661
|
+
} catch {
|
|
3662
|
+
return null;
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
function normalizeSourcePath(source) {
|
|
3666
|
+
let s = source.replace(/[?#].*$/, "");
|
|
3667
|
+
s = s.replace(/^webpack:\/\/[^/]*\//, "").replace(/^webpack:\/\//, "");
|
|
3668
|
+
s = s.replace(/^(?:vite|rollup|turbopack|ng):\/*/, "");
|
|
3669
|
+
s = s.replace(/^\[project\]\//, "");
|
|
3670
|
+
s = s.replace(/^file:\/\//, "");
|
|
3671
|
+
s = s.replace(/^\/@fs\//, "/");
|
|
3672
|
+
s = s.replace(/^\/@id\//, "");
|
|
3673
|
+
try {
|
|
3674
|
+
s = decodeURIComponent(s);
|
|
3675
|
+
} catch {
|
|
3676
|
+
}
|
|
3677
|
+
s = s.replace(/\\/g, "/");
|
|
3678
|
+
if (/^\/[A-Za-z]:\//.test(s)) s = s.slice(1);
|
|
3679
|
+
s = s.replace(/(^|\/)\.\//g, "$1");
|
|
3680
|
+
return s;
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
// ../core/src/code-reach.ts
|
|
3684
|
+
var MAX_CODE_REACH_FILES = 2e3;
|
|
3685
|
+
function executedFunctionRanges(entry) {
|
|
3686
|
+
const length = entry.source?.length ?? Number.POSITIVE_INFINITY;
|
|
3687
|
+
const out = [];
|
|
3688
|
+
entry.functions.forEach((fn, i) => {
|
|
3689
|
+
const whole = fn.ranges[0];
|
|
3690
|
+
if (!whole || whole.count <= 0) return;
|
|
3691
|
+
const topLevel = whole.startOffset === 0 && (whole.endOffset >= length || i === 0 && fn.functionName === "");
|
|
3692
|
+
if (topLevel) return;
|
|
3693
|
+
out.push({ start: whole.startOffset, end: whole.endOffset });
|
|
3694
|
+
});
|
|
3695
|
+
return out;
|
|
3696
|
+
}
|
|
3697
|
+
function executedFunctionOffsets(entry) {
|
|
3698
|
+
return executedFunctionRanges(entry).map((range) => range.start);
|
|
3699
|
+
}
|
|
3700
|
+
function scriptRan(entry) {
|
|
3701
|
+
return executedFunctionOffsets(entry).length > 0;
|
|
3702
|
+
}
|
|
3703
|
+
function reachedSources(entry, map) {
|
|
3704
|
+
const ranges = executedFunctionRanges(entry);
|
|
3705
|
+
const reached = /* @__PURE__ */ new Set();
|
|
3706
|
+
if (!ranges.length || entry.source === void 0) return reached;
|
|
3707
|
+
const starts = lineStarts(entry.source);
|
|
3708
|
+
for (const range of ranges) {
|
|
3709
|
+
const start = offsetToPosition(starts, range.start);
|
|
3710
|
+
const source = map.sourceAt(start.line, start.column);
|
|
3711
|
+
if (source === null || reached.has(source)) continue;
|
|
3712
|
+
if (map.mapsBetween(source, start, offsetToPosition(starts, range.end))) reached.add(source);
|
|
3713
|
+
}
|
|
3714
|
+
return reached;
|
|
3715
|
+
}
|
|
3716
|
+
function finalizeCodeReach(files) {
|
|
3717
|
+
const out = /* @__PURE__ */ new Set();
|
|
3718
|
+
for (const raw of files) {
|
|
3719
|
+
const file = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
3720
|
+
if (!file || file.startsWith("../") || file.startsWith("/") || /^[A-Za-z]:\//.test(file)) continue;
|
|
3721
|
+
if (/(^|\/)node_modules\//.test(file)) continue;
|
|
3722
|
+
out.add(file);
|
|
3723
|
+
}
|
|
3724
|
+
return [...out].sort().slice(0, MAX_CODE_REACH_FILES);
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
// src/internal/capture/code-reach.ts
|
|
3728
|
+
var MAX_MAP_BYTES = 20 * 1024 * 1024;
|
|
3729
|
+
var MAP_TIMEOUT_MS = 5e3;
|
|
3730
|
+
var SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|marko|html?)$/i;
|
|
3731
|
+
var REPLAYED_SCRIPT = /^(?:about|rsc):/i;
|
|
3732
|
+
var BUILD_DIRS = /(?:^|\/)(?:dist|build|out|\.output|\.next|\.nuxt|\.svelte-kit|public|static|wwwroot)\//;
|
|
3733
|
+
var MAX_CACHED_SCRIPTS = 500;
|
|
3734
|
+
var LatestByUrl = class {
|
|
3735
|
+
constructor() {
|
|
3736
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
3737
|
+
}
|
|
3738
|
+
get(url, version) {
|
|
3739
|
+
const entry = this.entries.get(url);
|
|
3740
|
+
if (!entry || entry.version !== version) return void 0;
|
|
3741
|
+
this.entries.delete(url);
|
|
3742
|
+
this.entries.set(url, entry);
|
|
3743
|
+
return entry.value;
|
|
3744
|
+
}
|
|
3745
|
+
set(url, version, value) {
|
|
3746
|
+
this.entries.delete(url);
|
|
3747
|
+
this.entries.set(url, { version, value });
|
|
3748
|
+
if (this.entries.size > MAX_CACHED_SCRIPTS) this.entries.delete(this.entries.keys().next().value);
|
|
3749
|
+
}
|
|
3750
|
+
clear() {
|
|
3751
|
+
this.entries.clear();
|
|
3752
|
+
}
|
|
3753
|
+
};
|
|
3754
|
+
var scriptCache = new LatestByUrl();
|
|
3755
|
+
var mapCache = new LatestByUrl();
|
|
3756
|
+
var cachedRepoRoot = null;
|
|
3757
|
+
function repositoryRoot(from) {
|
|
3758
|
+
if (cachedRepoRoot?.from === from) return cachedRepoRoot.root;
|
|
3759
|
+
let root = from;
|
|
3760
|
+
try {
|
|
3761
|
+
root = (0, import_node_child_process.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
|
|
3762
|
+
cwd: from,
|
|
3763
|
+
encoding: "utf-8",
|
|
3764
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3765
|
+
}).trim();
|
|
3766
|
+
} catch {
|
|
3767
|
+
}
|
|
3768
|
+
cachedRepoRoot = { from, root: path9.resolve(root) };
|
|
3769
|
+
return cachedRepoRoot.root;
|
|
3770
|
+
}
|
|
3771
|
+
function codeReachRoots(configDir, configured) {
|
|
3772
|
+
const repoRoot = repositoryRoot(configDir);
|
|
3773
|
+
const roots = configured?.length ? configured.map((r) => path9.resolve(configDir, r)) : [configDir];
|
|
3774
|
+
return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
|
|
3775
|
+
}
|
|
3776
|
+
function isFile(file) {
|
|
3777
|
+
try {
|
|
3778
|
+
return fs6.statSync(file).isFile();
|
|
3779
|
+
} catch {
|
|
3780
|
+
return false;
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
function resolveSourcePath(source, roots) {
|
|
3784
|
+
const normalized = normalizeSourcePath(source);
|
|
3785
|
+
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
3786
|
+
if (path9.isAbsolute(normalized) && isFile(normalized)) return path9.resolve(normalized);
|
|
3787
|
+
const relative7 = normalized.replace(/^\/+/, "");
|
|
3788
|
+
for (const root of roots) {
|
|
3789
|
+
const candidate = path9.resolve(root, relative7);
|
|
3790
|
+
if (isFile(candidate)) return candidate;
|
|
3791
|
+
}
|
|
3792
|
+
return null;
|
|
3793
|
+
}
|
|
3794
|
+
function viteBases(entries) {
|
|
3795
|
+
const bases = /* @__PURE__ */ new Map();
|
|
3796
|
+
for (const entry of entries) {
|
|
3797
|
+
const match = /^(https?:\/\/[^/]+)(\/.*?)@vite\/client$/.exec(entry.url.replace(/[?#].*$/, ""));
|
|
3798
|
+
if (match && match[2] !== "/") bases.set(match[1], match[2]);
|
|
3799
|
+
}
|
|
3800
|
+
return bases;
|
|
3801
|
+
}
|
|
3802
|
+
function fileForUrl(url, roots, bases) {
|
|
3803
|
+
let pathname;
|
|
3804
|
+
try {
|
|
3805
|
+
const parsed = new URL(url);
|
|
3806
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" && parsed.protocol !== "file:") return null;
|
|
3807
|
+
pathname = parsed.pathname;
|
|
3808
|
+
const base = bases.get(parsed.origin);
|
|
3809
|
+
if (base && pathname.startsWith(base)) pathname = pathname.slice(base.length - 1);
|
|
3810
|
+
} catch {
|
|
3811
|
+
return null;
|
|
3812
|
+
}
|
|
3813
|
+
if (!SOURCE_EXTENSIONS.test(pathname) || pathname.startsWith("/@vite/") || pathname.includes("/node_modules/")) {
|
|
3814
|
+
return null;
|
|
3815
|
+
}
|
|
3816
|
+
const file = resolveSourcePath(pathname, roots);
|
|
3817
|
+
if (!file || BUILD_DIRS.test(file.replace(/\\/g, "/"))) return null;
|
|
3818
|
+
return file;
|
|
3819
|
+
}
|
|
3820
|
+
function relativeToRepo(file, repoRoot) {
|
|
3821
|
+
return path9.relative(repoRoot, file).split(path9.sep).join("/");
|
|
3822
|
+
}
|
|
3823
|
+
function scriptHash(entry) {
|
|
3824
|
+
return crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
3825
|
+
}
|
|
3826
|
+
async function loadMap(entry, fetchMap) {
|
|
3827
|
+
const ref = entry.source ? sourceMappingUrl(entry.source) : null;
|
|
3828
|
+
if (!ref) return null;
|
|
3829
|
+
if (ref.startsWith("data:")) {
|
|
3830
|
+
const text = decodeDataUrl(ref);
|
|
3831
|
+
return text ? parseMap(text) : null;
|
|
3832
|
+
}
|
|
3833
|
+
let absolute;
|
|
3834
|
+
try {
|
|
3835
|
+
absolute = new URL(ref, entry.url).href;
|
|
3836
|
+
} catch {
|
|
3837
|
+
return null;
|
|
3838
|
+
}
|
|
3839
|
+
const version = scriptHash(entry);
|
|
3840
|
+
let pending = mapCache.get(absolute, version);
|
|
3841
|
+
if (!pending) {
|
|
3842
|
+
pending = fetchMap(absolute).then((text) => text ? parseMap(text) : null);
|
|
3843
|
+
mapCache.set(absolute, version, pending);
|
|
3844
|
+
}
|
|
3845
|
+
return pending;
|
|
3846
|
+
}
|
|
3847
|
+
function parseMap(text) {
|
|
3848
|
+
try {
|
|
3849
|
+
return decodeSourceMap(JSON.parse(text));
|
|
3850
|
+
} catch {
|
|
3851
|
+
return null;
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
async function resolveScript(entry, roots, bases, fetchMap) {
|
|
3855
|
+
const version = scriptHash(entry);
|
|
3856
|
+
const cached = scriptCache.get(entry.url, version);
|
|
3857
|
+
if (cached) return cached;
|
|
3858
|
+
let resolution;
|
|
3859
|
+
const file = fileForUrl(entry.url, roots.roots, bases);
|
|
3860
|
+
if (file) resolution = { kind: "file", file, lookup: await loadMap(entry, fetchMap) };
|
|
3861
|
+
else {
|
|
3862
|
+
const lookup = await loadMap(entry, fetchMap);
|
|
3863
|
+
resolution = lookup ? { kind: "map", lookup, files: [] } : { kind: "none" };
|
|
3864
|
+
}
|
|
3865
|
+
scriptCache.set(entry.url, version, resolution);
|
|
3866
|
+
return resolution;
|
|
3867
|
+
}
|
|
3868
|
+
async function resolveCodeReach(entries, roots, fetchMap) {
|
|
3869
|
+
const files = /* @__PURE__ */ new Set();
|
|
3870
|
+
const bases = viteBases(entries);
|
|
3871
|
+
for (const entry of entries) {
|
|
3872
|
+
if (!entry.url || !entry.functions?.length || REPLAYED_SCRIPT.test(entry.url)) continue;
|
|
3873
|
+
const resolution = await resolveScript(entry, roots, bases, fetchMap);
|
|
3874
|
+
if (resolution.kind === "file") {
|
|
3875
|
+
const ran = resolution.lookup ? reachedSources(entry, resolution.lookup).size > 0 : scriptRan(entry);
|
|
3876
|
+
if (resolution.file && ran) files.add(relativeToRepo(resolution.file, roots.repoRoot));
|
|
3877
|
+
} else if (resolution.kind === "map") {
|
|
3878
|
+
for (const index of reachedSources(entry, resolution.lookup)) {
|
|
3879
|
+
if (resolution.files[index] === void 0) {
|
|
3880
|
+
const source = resolution.lookup.sources[index] ?? "";
|
|
3881
|
+
let absolute = resolveSourcePath(source, roots.roots);
|
|
3882
|
+
if (!absolute && !/^[a-z][\w+.-]*:/i.test(source)) {
|
|
3883
|
+
try {
|
|
3884
|
+
absolute = fileForUrl(new URL(source, entry.url).href, roots.roots, bases);
|
|
3885
|
+
} catch {
|
|
3886
|
+
absolute = null;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
resolution.files[index] = absolute;
|
|
3890
|
+
}
|
|
3891
|
+
const file = resolution.files[index];
|
|
3892
|
+
if (file) files.add(relativeToRepo(file, roots.repoRoot));
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
return finalizeCodeReach(files);
|
|
3897
|
+
}
|
|
3898
|
+
function pageMapFetcher(page) {
|
|
3899
|
+
return async (url) => {
|
|
3900
|
+
try {
|
|
3901
|
+
const response = await internalCall(page, () => page.request.get(url, { timeout: MAP_TIMEOUT_MS }));
|
|
3902
|
+
if (!response.ok()) return null;
|
|
3903
|
+
const length = Number(response.headers()["content-length"] ?? 0);
|
|
3904
|
+
if (length > MAX_MAP_BYTES) return null;
|
|
3905
|
+
const body = await response.body();
|
|
3906
|
+
return body.length > MAX_MAP_BYTES ? null : body.toString("utf-8");
|
|
3907
|
+
} catch {
|
|
3908
|
+
return null;
|
|
3909
|
+
}
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
function isChromiumPage(page) {
|
|
3913
|
+
try {
|
|
3914
|
+
return page.context().browser()?.browserType().name() === "chromium";
|
|
3915
|
+
} catch {
|
|
3916
|
+
return false;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
async function startCodeReach(page) {
|
|
3920
|
+
if (!isChromiumPage(page) || !page.coverage) return false;
|
|
3921
|
+
try {
|
|
3922
|
+
await internalCall(
|
|
3923
|
+
page,
|
|
3924
|
+
() => page.coverage.startJSCoverage({ resetOnNavigation: false, reportAnonymousScripts: false })
|
|
3925
|
+
);
|
|
3926
|
+
return true;
|
|
3927
|
+
} catch {
|
|
3928
|
+
return false;
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
async function stopCodeReach(page) {
|
|
3932
|
+
try {
|
|
3933
|
+
return await internalCall(page, () => page.coverage.stopJSCoverage());
|
|
3934
|
+
} catch {
|
|
3935
|
+
return null;
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
|
|
2990
3939
|
// src/internal/capture/capture-fixtures.ts
|
|
2991
3940
|
function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
|
|
2992
3941
|
const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
|
|
@@ -3036,11 +3985,53 @@ function createSink() {
|
|
|
3036
3985
|
pageInventories: [],
|
|
3037
3986
|
probeItem: null,
|
|
3038
3987
|
probeInterception: null,
|
|
3988
|
+
flake: null,
|
|
3039
3989
|
pickOffered: false,
|
|
3040
3990
|
userPick: null,
|
|
3041
|
-
locatorPages: new LocatorPageLog()
|
|
3991
|
+
locatorPages: new LocatorPageLog(),
|
|
3992
|
+
codeReach: null
|
|
3042
3993
|
};
|
|
3043
3994
|
}
|
|
3995
|
+
function maybeStartCodeReach(page) {
|
|
3996
|
+
const sink = currentSink;
|
|
3997
|
+
if (!sink || process.env.PIWI_CAPTURE_CODE_REACH !== "true") return Promise.resolve(false);
|
|
3998
|
+
if (sink.codeReach) return sink.codeReach.page === page ? sink.codeReach.started : Promise.resolve(false);
|
|
3999
|
+
const started = startCodeReach(page);
|
|
4000
|
+
sink.codeReach = { page, started, stopped: false, files: null };
|
|
4001
|
+
return started;
|
|
4002
|
+
}
|
|
4003
|
+
function configuredCodeReachRoots() {
|
|
4004
|
+
const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
|
|
4005
|
+
if (!raw) return null;
|
|
4006
|
+
try {
|
|
4007
|
+
const parsed = JSON.parse(raw);
|
|
4008
|
+
if (Array.isArray(parsed)) return parsed.filter((r) => typeof r === "string" && r !== "");
|
|
4009
|
+
} catch {
|
|
4010
|
+
}
|
|
4011
|
+
return raw.split(path10.delimiter).filter(Boolean);
|
|
4012
|
+
}
|
|
4013
|
+
async function stopSinkCodeReach(sink, closing) {
|
|
4014
|
+
const session = sink.codeReach;
|
|
4015
|
+
if (!session || session.stopped) return;
|
|
4016
|
+
if (closing && closing.page !== session.page && (!closing.context || pageContext(session.page) !== closing.context)) {
|
|
4017
|
+
return;
|
|
4018
|
+
}
|
|
4019
|
+
session.stopped = true;
|
|
4020
|
+
try {
|
|
4021
|
+
if (!await session.started || isPageClosed(session.page)) return;
|
|
4022
|
+
const entries = await stopCodeReach(session.page);
|
|
4023
|
+
if (!entries) return;
|
|
4024
|
+
const info = sink.testInfo;
|
|
4025
|
+
const configFile = info?.config.configFile;
|
|
4026
|
+
const configDir = configFile ? path10.dirname(configFile) : info?.config.rootDir ?? process.cwd();
|
|
4027
|
+
session.files = await resolveCodeReach(
|
|
4028
|
+
entries,
|
|
4029
|
+
codeReachRoots(configDir, configuredCodeReachRoots()),
|
|
4030
|
+
pageMapFetcher(session.page)
|
|
4031
|
+
);
|
|
4032
|
+
} catch {
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
3044
4035
|
var currentSink = null;
|
|
3045
4036
|
var PENDING_PROBES = /* @__PURE__ */ new Set();
|
|
3046
4037
|
async function drainPendingProbes(capMs) {
|
|
@@ -3048,8 +4039,8 @@ async function drainPendingProbes(capMs) {
|
|
|
3048
4039
|
let cap;
|
|
3049
4040
|
await Promise.race([
|
|
3050
4041
|
Promise.allSettled(PENDING_PROBES),
|
|
3051
|
-
new Promise((
|
|
3052
|
-
cap = setTimeout(
|
|
4042
|
+
new Promise((resolve3) => {
|
|
4043
|
+
cap = setTimeout(resolve3, capMs);
|
|
3053
4044
|
})
|
|
3054
4045
|
]);
|
|
3055
4046
|
clearTimeout(cap);
|
|
@@ -3089,10 +4080,10 @@ async function readWebVitals(page) {
|
|
|
3089
4080
|
const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
|
|
3090
4081
|
paint[key] = Math.round(entry.startTime);
|
|
3091
4082
|
}
|
|
3092
|
-
const readBuffered = (type, extra) => new Promise((
|
|
4083
|
+
const readBuffered = (type, extra) => new Promise((resolve3) => {
|
|
3093
4084
|
try {
|
|
3094
4085
|
const PO = globalThis.PerformanceObserver;
|
|
3095
|
-
if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return
|
|
4086
|
+
if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve3(null);
|
|
3096
4087
|
const out = [];
|
|
3097
4088
|
const po = new PO((list) => out.push(...list.getEntries()));
|
|
3098
4089
|
po.observe({ type, buffered: true, ...extra });
|
|
@@ -3102,10 +4093,10 @@ async function readWebVitals(page) {
|
|
|
3102
4093
|
po.disconnect();
|
|
3103
4094
|
} catch {
|
|
3104
4095
|
}
|
|
3105
|
-
|
|
4096
|
+
resolve3(out);
|
|
3106
4097
|
}, 0);
|
|
3107
4098
|
} catch {
|
|
3108
|
-
|
|
4099
|
+
resolve3(null);
|
|
3109
4100
|
}
|
|
3110
4101
|
});
|
|
3111
4102
|
const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
|
|
@@ -3218,12 +4209,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
|
|
|
3218
4209
|
var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
|
|
3219
4210
|
var MAX_PAGE_INVENTORY_WINDOWS = 64;
|
|
3220
4211
|
function withTimeout(promise, ms) {
|
|
3221
|
-
return new Promise((
|
|
4212
|
+
return new Promise((resolve3, reject) => {
|
|
3222
4213
|
const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
|
|
3223
4214
|
promise.then(
|
|
3224
4215
|
(value) => {
|
|
3225
4216
|
clearTimeout(timer);
|
|
3226
|
-
|
|
4217
|
+
resolve3(value);
|
|
3227
4218
|
},
|
|
3228
4219
|
(error) => {
|
|
3229
4220
|
clearTimeout(timer);
|
|
@@ -3550,9 +4541,11 @@ function wrapFrameLocator(page, frameLocator) {
|
|
|
3550
4541
|
}
|
|
3551
4542
|
});
|
|
3552
4543
|
}
|
|
4544
|
+
var CANCELLED_REQUEST = /^net::ERR_ABORTED$|^NS_BINDING_ABORTED$|^NS_ERROR_ABORT$|cancell?ed/i;
|
|
3553
4545
|
function instrumentPage(page) {
|
|
3554
4546
|
if (!page || INSTRUMENTED_PAGES.has(page)) return;
|
|
3555
4547
|
INSTRUMENTED_PAGES.add(page);
|
|
4548
|
+
void maybeStartCodeReach(page);
|
|
3556
4549
|
const probeSink = currentSink;
|
|
3557
4550
|
if (probeSink?.probeItem && !probeSink.probeInterception) {
|
|
3558
4551
|
void installProbeInterception(page, probeSink.probeItem).then((interception) => {
|
|
@@ -3560,6 +4553,18 @@ function instrumentPage(page) {
|
|
|
3560
4553
|
}).catch(() => {
|
|
3561
4554
|
});
|
|
3562
4555
|
}
|
|
4556
|
+
const flake = currentSink?.flake;
|
|
4557
|
+
if (flake?.role === "target" && !flake.conditions) {
|
|
4558
|
+
flake.browserName = pageBrowserName(page) ?? flake.browserName;
|
|
4559
|
+
const plan = flake.plan;
|
|
4560
|
+
flake.conditions = installFlakeConditions(page, plan).catch((error) => {
|
|
4561
|
+
const note = `conditions failed to install: ${error instanceof Error ? error.message : String(error)}`;
|
|
4562
|
+
const reports = unappliedReports(plan).map(
|
|
4563
|
+
(r) => r.outcome === "by-command" ? r : { ...r, outcome: "skipped", note }
|
|
4564
|
+
);
|
|
4565
|
+
return { reports: () => reports };
|
|
4566
|
+
});
|
|
4567
|
+
}
|
|
3563
4568
|
const ctx = pageContext(page);
|
|
3564
4569
|
if (ctx) instrumentContext(ctx);
|
|
3565
4570
|
if (typeof page.close === "function") {
|
|
@@ -3570,6 +4575,7 @@ function instrumentPage(page) {
|
|
|
3570
4575
|
if (sink) {
|
|
3571
4576
|
await stashPageState(sink, { page });
|
|
3572
4577
|
await maybeOpenPicker(sink, { page });
|
|
4578
|
+
await stopSinkCodeReach(sink, { page });
|
|
3573
4579
|
}
|
|
3574
4580
|
return originalClose(...args);
|
|
3575
4581
|
};
|
|
@@ -3641,7 +4647,7 @@ function instrumentPage(page) {
|
|
|
3641
4647
|
} catch {
|
|
3642
4648
|
}
|
|
3643
4649
|
}
|
|
3644
|
-
|
|
4650
|
+
const recordRequest = (request, failed) => {
|
|
3645
4651
|
const sink = currentSink;
|
|
3646
4652
|
if (!sink) return;
|
|
3647
4653
|
sink.lastActivePage = page;
|
|
@@ -3649,18 +4655,21 @@ function instrumentPage(page) {
|
|
|
3649
4655
|
try {
|
|
3650
4656
|
const url = request.url();
|
|
3651
4657
|
if (url.startsWith("data:") || url.startsWith("blob:")) return;
|
|
3652
|
-
const timing = request.timing();
|
|
3653
|
-
const response = await request.response();
|
|
3654
4658
|
const resourceType = request.resourceType();
|
|
3655
|
-
if (!
|
|
4659
|
+
if (!TRACKED_REQUEST_TYPES.includes(resourceType)) return;
|
|
4660
|
+
const failure = failed ? request.failure()?.errorText ?? "failed" : null;
|
|
4661
|
+
if (failure && CANCELLED_REQUEST.test(failure)) return;
|
|
4662
|
+
const timing = request.timing();
|
|
4663
|
+
const response = failed ? null : await request.response();
|
|
3656
4664
|
const entry = {
|
|
3657
4665
|
method: request.method(),
|
|
3658
4666
|
url,
|
|
3659
4667
|
status: response ? response.status() : 0,
|
|
3660
|
-
duration:
|
|
4668
|
+
duration: requestDuration(timing, failed),
|
|
3661
4669
|
startTime: timing.startTime,
|
|
3662
4670
|
resourceType
|
|
3663
4671
|
};
|
|
4672
|
+
if (failure) entry.failure = failure;
|
|
3664
4673
|
if (response) {
|
|
3665
4674
|
const headers = response.headers();
|
|
3666
4675
|
const contentType = headers["content-type"];
|
|
@@ -3685,7 +4694,12 @@ function instrumentPage(page) {
|
|
|
3685
4694
|
}
|
|
3686
4695
|
})();
|
|
3687
4696
|
sink.pendingHandlers.push(p);
|
|
3688
|
-
}
|
|
4697
|
+
};
|
|
4698
|
+
page.on("requestfinished", (request) => recordRequest(request, false));
|
|
4699
|
+
page.on("requestfailed", (request) => recordRequest(request, true));
|
|
4700
|
+
}
|
|
4701
|
+
async function awaitFlakeConditions() {
|
|
4702
|
+
await currentSink?.flake?.conditions;
|
|
3689
4703
|
}
|
|
3690
4704
|
function instrumentContext(context) {
|
|
3691
4705
|
if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
|
|
@@ -3698,6 +4712,8 @@ function instrumentContext(context) {
|
|
|
3698
4712
|
context.newPage = async (...args) => {
|
|
3699
4713
|
const page = await originalNewPage(...args);
|
|
3700
4714
|
instrumentPage(page);
|
|
4715
|
+
await awaitFlakeConditions();
|
|
4716
|
+
await maybeStartCodeReach(page);
|
|
3701
4717
|
return page;
|
|
3702
4718
|
};
|
|
3703
4719
|
if (typeof context.close === "function") {
|
|
@@ -3708,6 +4724,7 @@ function instrumentContext(context) {
|
|
|
3708
4724
|
if (sink) {
|
|
3709
4725
|
await stashPageState(sink, { context });
|
|
3710
4726
|
await maybeOpenPicker(sink, { context });
|
|
4727
|
+
await stopSinkCodeReach(sink, { context });
|
|
3711
4728
|
}
|
|
3712
4729
|
return originalClose(...args);
|
|
3713
4730
|
};
|
|
@@ -3721,6 +4738,8 @@ function patchBrowser(browser) {
|
|
|
3721
4738
|
browser.newPage = async (...args) => {
|
|
3722
4739
|
const page = await originalNewPage(...args);
|
|
3723
4740
|
instrumentPage(page);
|
|
4741
|
+
await awaitFlakeConditions();
|
|
4742
|
+
await maybeStartCodeReach(page);
|
|
3724
4743
|
return page;
|
|
3725
4744
|
};
|
|
3726
4745
|
const originalNewContext = browser.newContext.bind(browser);
|
|
@@ -3743,13 +4762,19 @@ function readRootAria(page) {
|
|
|
3743
4762
|
function readRootAriaJson(page) {
|
|
3744
4763
|
return internalCall(page, () => ariaSnapshotJSONBestEffort(page.locator(":root")));
|
|
3745
4764
|
}
|
|
4765
|
+
var TRACKED_REQUEST_TYPES = ["fetch", "xhr", "document", "other"];
|
|
4766
|
+
function requestDuration(timing, failed) {
|
|
4767
|
+
if (timing.responseEnd > 0) return Math.round(timing.responseEnd - timing.requestStart);
|
|
4768
|
+
if (failed && timing.startTime > 0) return Math.max(0, Math.round(Date.now() - timing.startTime));
|
|
4769
|
+
return 0;
|
|
4770
|
+
}
|
|
3746
4771
|
async function flushSink(sink, testInfo) {
|
|
3747
4772
|
await Promise.allSettled(sink.pendingHandlers);
|
|
3748
4773
|
let drainDeadline;
|
|
3749
4774
|
await Promise.race([
|
|
3750
4775
|
Promise.allSettled(sink.capturePromises),
|
|
3751
|
-
new Promise((
|
|
3752
|
-
drainDeadline = setTimeout(
|
|
4776
|
+
new Promise((resolve3) => {
|
|
4777
|
+
drainDeadline = setTimeout(resolve3, 2e3);
|
|
3753
4778
|
})
|
|
3754
4779
|
]);
|
|
3755
4780
|
clearTimeout(drainDeadline);
|
|
@@ -3765,6 +4790,13 @@ async function flushSink(sink, testInfo) {
|
|
|
3765
4790
|
body: Buffer.from(JSON.stringify(dedupeSnapshotsByLocation(sink.capturedLocators)))
|
|
3766
4791
|
});
|
|
3767
4792
|
}
|
|
4793
|
+
await stopSinkCodeReach(sink);
|
|
4794
|
+
if (sink.codeReach?.files?.length) {
|
|
4795
|
+
await testInfo.attach(ATTACHMENT_NAMES.codeReach, {
|
|
4796
|
+
contentType: "application/json",
|
|
4797
|
+
body: Buffer.from(JSON.stringify(sink.codeReach.files))
|
|
4798
|
+
});
|
|
4799
|
+
}
|
|
3768
4800
|
if (sink.locatorPages.size > 0) {
|
|
3769
4801
|
await testInfo.attach(ATTACHMENT_NAMES.locatorPages, {
|
|
3770
4802
|
contentType: "application/json",
|
|
@@ -3898,6 +4930,8 @@ var piwiFixtures = {
|
|
|
3898
4930
|
// never double-wraps a page the browser patch already instrumented.
|
|
3899
4931
|
page: async ({ page }, use) => {
|
|
3900
4932
|
instrumentPage(page);
|
|
4933
|
+
await awaitFlakeConditions();
|
|
4934
|
+
await maybeStartCodeReach(page);
|
|
3901
4935
|
await use(page);
|
|
3902
4936
|
},
|
|
3903
4937
|
// Auto, test-scoped: open a capture sink for the running test and flush it
|
|
@@ -3906,8 +4940,23 @@ var piwiFixtures = {
|
|
|
3906
4940
|
piwiCapture: [
|
|
3907
4941
|
async ({}, use, testInfo) => {
|
|
3908
4942
|
boxCaptureFrames();
|
|
4943
|
+
const conflict = labModeConflict();
|
|
4944
|
+
if (conflict) throw new Error(conflict);
|
|
3909
4945
|
const sink = createSink();
|
|
3910
4946
|
sink.testInfo = testInfo;
|
|
4947
|
+
if (isFlakeMode()) {
|
|
4948
|
+
const plan = loadFlakePlan();
|
|
4949
|
+
const role = flakeRoleForTest(plan, {
|
|
4950
|
+
file: testInfo.file,
|
|
4951
|
+
title: testInfo.title,
|
|
4952
|
+
titlePath: testInfo.titlePath,
|
|
4953
|
+
project: testInfo.project.name
|
|
4954
|
+
});
|
|
4955
|
+
if (role) {
|
|
4956
|
+
const browserName = testInfo.project.use?.browserName ?? null;
|
|
4957
|
+
sink.flake = { plan, role, startedAt: Date.now(), browserName, conditions: null };
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
3911
4960
|
if (isProbeMode())
|
|
3912
4961
|
sink.probeItem = probeItemForTest({
|
|
3913
4962
|
title: testInfo.title,
|
|
@@ -3938,12 +4987,34 @@ var piwiFixtures = {
|
|
|
3938
4987
|
handled
|
|
3939
4988
|
});
|
|
3940
4989
|
}
|
|
4990
|
+
if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
|
|
3941
4991
|
await flushSink(sink, testInfo);
|
|
3942
4992
|
}
|
|
3943
4993
|
},
|
|
3944
4994
|
{ auto: true }
|
|
3945
4995
|
]
|
|
3946
4996
|
};
|
|
4997
|
+
async function recordFlakeAttempt(flake, testInfo) {
|
|
4998
|
+
const installed = flake.conditions ? await flake.conditions : null;
|
|
4999
|
+
recordFlakeResult(
|
|
5000
|
+
flakeResultLine(flake.plan, {
|
|
5001
|
+
role: flake.role,
|
|
5002
|
+
file: testInfo.file,
|
|
5003
|
+
title: testInfo.title,
|
|
5004
|
+
project: testInfo.project.name || null,
|
|
5005
|
+
browserName: flake.browserName,
|
|
5006
|
+
status: testInfo.status ?? "passed",
|
|
5007
|
+
errorText: joinErrorMessages(testInfo.errors),
|
|
5008
|
+
startedAt: flake.startedAt,
|
|
5009
|
+
duration: Date.now() - flake.startedAt,
|
|
5010
|
+
workerIndex: testInfo.workerIndex,
|
|
5011
|
+
parallelIndex: testInfo.parallelIndex,
|
|
5012
|
+
repeatEachIndex: testInfo.repeatEachIndex,
|
|
5013
|
+
retry: testInfo.retry,
|
|
5014
|
+
conditions: installed ? installed.reports() : unappliedReports(flake.plan)
|
|
5015
|
+
})
|
|
5016
|
+
);
|
|
5017
|
+
}
|
|
3947
5018
|
function extendPiwiFixtures(test) {
|
|
3948
5019
|
return test.extend(piwiFixtures);
|
|
3949
5020
|
}
|
|
@@ -3956,11 +5027,17 @@ function extendPiwiFixtures(test) {
|
|
|
3956
5027
|
boxCaptureFrames,
|
|
3957
5028
|
buildPageState,
|
|
3958
5029
|
callMethod,
|
|
5030
|
+
codeReachRoots,
|
|
3959
5031
|
computeCoreVitals,
|
|
5032
|
+
configuredCodeReachRoots,
|
|
3960
5033
|
extendPiwiFixtures,
|
|
3961
5034
|
internalCall,
|
|
3962
5035
|
noteLocatorCall,
|
|
3963
5036
|
noteNavigation,
|
|
5037
|
+
pageMapFetcher,
|
|
3964
5038
|
piwiFixtures,
|
|
3965
|
-
probeElementAttrs
|
|
5039
|
+
probeElementAttrs,
|
|
5040
|
+
resolveCodeReach,
|
|
5041
|
+
startCodeReach,
|
|
5042
|
+
stopCodeReach
|
|
3966
5043
|
});
|