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