@piwitests/reporter 0.43.0 → 0.45.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 +89 -15
- package/dist/global-setup-module.js +6 -0
- package/dist/index.d.ts +55 -1
- package/dist/index.js +2481 -323
- package/dist/internal/capture/attachments.d.ts +1 -0
- package/dist/internal/capture/attachments.js +2 -1
- package/dist/internal/capture/capture-fixtures.d.ts +374 -5
- package/dist/internal/capture/capture-fixtures.js +949 -36
- package/package.json +1 -1
|
@@ -32,6 +32,7 @@ var capture_fixtures_exports = {};
|
|
|
32
32
|
__export(capture_fixtures_exports, {
|
|
33
33
|
CAPTURED_ATTRS_ARG: () => CAPTURED_ATTRS_ARG,
|
|
34
34
|
LocatorPageLog: () => LocatorPageLog,
|
|
35
|
+
activeResourceLedger: () => activeResourceLedger,
|
|
35
36
|
ariaSnapshotBestEffort: () => ariaSnapshotBestEffort,
|
|
36
37
|
ariaSnapshotJSONBestEffort: () => ariaSnapshotJSONBestEffort,
|
|
37
38
|
boxCaptureFrames: () => boxCaptureFrames,
|
|
@@ -44,15 +45,19 @@ __export(capture_fixtures_exports, {
|
|
|
44
45
|
internalCall: () => internalCall,
|
|
45
46
|
noteLocatorCall: () => noteLocatorCall,
|
|
46
47
|
noteNavigation: () => noteNavigation,
|
|
48
|
+
noteResourceUse: () => noteResourceUse,
|
|
47
49
|
pageMapFetcher: () => pageMapFetcher,
|
|
48
50
|
piwiFixtures: () => piwiFixtures,
|
|
49
51
|
probeElementAttrs: () => probeElementAttrs,
|
|
52
|
+
recordResourceCensus: () => recordResourceCensus,
|
|
50
53
|
resolveCodeReach: () => resolveCodeReach,
|
|
51
54
|
startCodeReach: () => startCodeReach,
|
|
52
|
-
|
|
55
|
+
startResourceLedger: () => startResourceLedger,
|
|
56
|
+
stopCodeReach: () => stopCodeReach,
|
|
57
|
+
stopResourceLedger: () => stopResourceLedger
|
|
53
58
|
});
|
|
54
59
|
module.exports = __toCommonJS(capture_fixtures_exports);
|
|
55
|
-
var
|
|
60
|
+
var path12 = __toESM(require("path"));
|
|
56
61
|
var import_node_zlib2 = require("zlib");
|
|
57
62
|
|
|
58
63
|
// ../picker-dom/src/probe.ts
|
|
@@ -134,7 +139,7 @@ function probeElementAttrs(el, arg) {
|
|
|
134
139
|
};
|
|
135
140
|
const roleMemo = /* @__PURE__ */ new Map();
|
|
136
141
|
const textMemo = /* @__PURE__ */ new Map();
|
|
137
|
-
const
|
|
142
|
+
const roleOf2 = (n) => {
|
|
138
143
|
const cached = roleMemo.get(n);
|
|
139
144
|
if (cached !== void 0) return cached;
|
|
140
145
|
let role;
|
|
@@ -157,7 +162,7 @@ function probeElementAttrs(el, arg) {
|
|
|
157
162
|
const al = n.getAttribute("aria-level");
|
|
158
163
|
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
159
164
|
};
|
|
160
|
-
const targetRole =
|
|
165
|
+
const targetRole = roleOf2(el);
|
|
161
166
|
const targetLevel = targetRole === "heading" ? levelOf(el) : null;
|
|
162
167
|
const nameOf = (n) => {
|
|
163
168
|
const labelledBy = !!n.getAttribute("aria-labelledby");
|
|
@@ -217,7 +222,7 @@ function probeElementAttrs(el, arg) {
|
|
|
217
222
|
let visibleRoleNameCount = 0;
|
|
218
223
|
for (let i = 0; i < nodes.length; i++) {
|
|
219
224
|
const n = nodes[i];
|
|
220
|
-
if (
|
|
225
|
+
if (roleOf2(n) !== targetRole) continue;
|
|
221
226
|
if (n === el) index = roleCountAll;
|
|
222
227
|
roleCountAll++;
|
|
223
228
|
if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
|
|
@@ -270,7 +275,7 @@ function probeElementAttrs(el, arg) {
|
|
|
270
275
|
};
|
|
271
276
|
const docRoleCount = (role) => {
|
|
272
277
|
let c = 0;
|
|
273
|
-
for (let i = 0; i < nodes.length; i++) if (
|
|
278
|
+
for (let i = 0; i < nodes.length; i++) if (roleOf2(nodes[i]) === role) c++;
|
|
274
279
|
return c;
|
|
275
280
|
};
|
|
276
281
|
const rawText = (n) => (n.textContent || "").replace(/\s+/g, " ").trim();
|
|
@@ -302,7 +307,7 @@ function probeElementAttrs(el, arg) {
|
|
|
302
307
|
const needle = text.toLowerCase();
|
|
303
308
|
let c = 0;
|
|
304
309
|
for (let i = 0; i < nodes.length; i++) {
|
|
305
|
-
if (
|
|
310
|
+
if (roleOf2(nodes[i]) !== role) continue;
|
|
306
311
|
if (normText(nodes[i]).indexOf(needle) !== -1) c++;
|
|
307
312
|
}
|
|
308
313
|
return c;
|
|
@@ -327,7 +332,7 @@ function probeElementAttrs(el, arg) {
|
|
|
327
332
|
scopedRoleCount = 0;
|
|
328
333
|
for (let i = 0; i < scoped.length; i++) {
|
|
329
334
|
const n = scoped[i];
|
|
330
|
-
if (
|
|
335
|
+
if (roleOf2(n) !== targetRole) continue;
|
|
331
336
|
if (targetLevel != null && levelOf(n) !== targetLevel) continue;
|
|
332
337
|
scopedRoleCount++;
|
|
333
338
|
}
|
|
@@ -675,7 +680,7 @@ function showAnchorPicker(arg) {
|
|
|
675
680
|
}
|
|
676
681
|
const Z = 2147483600;
|
|
677
682
|
const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
|
|
678
|
-
const
|
|
683
|
+
const roleOf2 = (n) => {
|
|
679
684
|
const explicit = n.getAttribute && n.getAttribute("role");
|
|
680
685
|
if (explicit) return explicit;
|
|
681
686
|
const tag = (n.tagName || "").toLowerCase();
|
|
@@ -698,7 +703,7 @@ function showAnchorPicker(arg) {
|
|
|
698
703
|
let matched = 0;
|
|
699
704
|
for (let i = 0; i < nodes.length; i++) {
|
|
700
705
|
const n = nodes[i];
|
|
701
|
-
if (
|
|
706
|
+
if (roleOf2(n) !== leafRole) continue;
|
|
702
707
|
if (leafLevel != null && levelOf(n) !== leafLevel) continue;
|
|
703
708
|
matched++;
|
|
704
709
|
}
|
|
@@ -731,7 +736,7 @@ function showAnchorPicker(arg) {
|
|
|
731
736
|
const testId = node.getAttribute("data-testid");
|
|
732
737
|
const id = node.getAttribute("id");
|
|
733
738
|
const ariaLabel = node.getAttribute("aria-label");
|
|
734
|
-
const role =
|
|
739
|
+
const role = roleOf2(node);
|
|
735
740
|
const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
|
|
736
741
|
if (testId) info.testIdCount = count(`[data-testid=${JSON.stringify(testId)}]`);
|
|
737
742
|
if (id) {
|
|
@@ -744,7 +749,7 @@ function showAnchorPicker(arg) {
|
|
|
744
749
|
let roleCount = 0;
|
|
745
750
|
let labeledCount = 0;
|
|
746
751
|
for (const n of roleNodes) {
|
|
747
|
-
if (
|
|
752
|
+
if (roleOf2(n) !== role) continue;
|
|
748
753
|
roleCount++;
|
|
749
754
|
if (ariaLabel && n.getAttribute && n.getAttribute("aria-label") === ariaLabel) labeledCount++;
|
|
750
755
|
}
|
|
@@ -793,7 +798,7 @@ function showAnchorPicker(arg) {
|
|
|
793
798
|
const nodes = Array.from(scope.querySelectorAll(roleSources));
|
|
794
799
|
if (nodes.length > 2e3) return [];
|
|
795
800
|
return nodes.filter(
|
|
796
|
-
(n) =>
|
|
801
|
+
(n) => roleOf2(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
|
|
797
802
|
);
|
|
798
803
|
} catch {
|
|
799
804
|
return [];
|
|
@@ -1798,7 +1803,8 @@ var ATTACHMENT_NAMES = {
|
|
|
1798
1803
|
pageState: "piwi-page-state",
|
|
1799
1804
|
userPick: "piwi-user-pick",
|
|
1800
1805
|
aiUsage: "piwi-ai-usage",
|
|
1801
|
-
aiMeta: "piwi-ai-meta"
|
|
1806
|
+
aiMeta: "piwi-ai-meta",
|
|
1807
|
+
resources: "piwi-resources"
|
|
1802
1808
|
};
|
|
1803
1809
|
var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
|
|
1804
1810
|
var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
@@ -1811,7 +1817,7 @@ function collectPageInventoryInPage(maxEntries) {
|
|
|
1811
1817
|
const doc = g.document;
|
|
1812
1818
|
if (!doc || !g.location) return null;
|
|
1813
1819
|
const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
1814
|
-
const
|
|
1820
|
+
const roleOf2 = (el) => {
|
|
1815
1821
|
const explicit = el.getAttribute("role");
|
|
1816
1822
|
if (explicit) return explicit.toLowerCase();
|
|
1817
1823
|
const tag = (el.tagName || "").toLowerCase();
|
|
@@ -1865,7 +1871,7 @@ function collectPageInventoryInPage(maxEntries) {
|
|
|
1865
1871
|
return text;
|
|
1866
1872
|
};
|
|
1867
1873
|
const nameOf = (el) => {
|
|
1868
|
-
const role =
|
|
1874
|
+
const role = roleOf2(el);
|
|
1869
1875
|
const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
|
|
1870
1876
|
if (!holdsValue(el, role)) chain.push(el.textContent);
|
|
1871
1877
|
chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
|
|
@@ -1883,7 +1889,7 @@ function collectPageInventoryInPage(maxEntries) {
|
|
|
1883
1889
|
const controlSelector = 'button, [role="button"], input, select, textarea, [role="checkbox"], [role="radio"], [role="tab"], [role="menuitem"], [role="combobox"], [role="switch"], [role="slider"], [role="searchbox"], [role="spinbutton"], [role="textbox"], [role="listbox"]';
|
|
1884
1890
|
for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
|
|
1885
1891
|
if (budget <= 0) break;
|
|
1886
|
-
const role =
|
|
1892
|
+
const role = roleOf2(el);
|
|
1887
1893
|
const name = nameOf(el);
|
|
1888
1894
|
if (!name) continue;
|
|
1889
1895
|
const key = `${role}\0${name}`;
|
|
@@ -2038,6 +2044,8 @@ var DEFAULTS = {
|
|
|
2038
2044
|
// Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
|
|
2039
2045
|
captureCodeReach: false,
|
|
2040
2046
|
captureServerTraces: true,
|
|
2047
|
+
captureResources: true,
|
|
2048
|
+
leakCheck: "report",
|
|
2041
2049
|
sampleAriaOnPass: true,
|
|
2042
2050
|
uploadManifest: true,
|
|
2043
2051
|
defaultCapture: true,
|
|
@@ -2078,6 +2086,8 @@ var PIWI_ENV_KEYS = {
|
|
|
2078
2086
|
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
2079
2087
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
2080
2088
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
2089
|
+
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
2090
|
+
leakCheck: "PIWI_LEAK_CHECK",
|
|
2081
2091
|
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
2082
2092
|
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
2083
2093
|
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
@@ -2103,6 +2113,7 @@ var PIWI_FLAKE_ENV = {
|
|
|
2103
2113
|
plan: "PIWI_FLAKE_PLAN",
|
|
2104
2114
|
results: "PIWI_FLAKE_RESULTS"
|
|
2105
2115
|
};
|
|
2116
|
+
var PIWI_RESOURCES_RESULTS_ENV = "PIWI_RESOURCES_RESULTS";
|
|
2106
2117
|
var ENV_FALLBACK_SPECS = [
|
|
2107
2118
|
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
2108
2119
|
{ option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
|
|
@@ -2127,6 +2138,8 @@ var ENV_FALLBACK_SPECS = [
|
|
|
2127
2138
|
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
2128
2139
|
{ option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
|
|
2129
2140
|
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
2141
|
+
{ option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
|
|
2142
|
+
{ option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
|
|
2130
2143
|
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
2131
2144
|
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
2132
2145
|
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
@@ -2142,8 +2155,8 @@ function parseRouteKey(routeKey2) {
|
|
|
2142
2155
|
return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
|
|
2143
2156
|
}
|
|
2144
2157
|
function patternToRegex(pattern) {
|
|
2145
|
-
const
|
|
2146
|
-
const escaped =
|
|
2158
|
+
const path13 = pattern.split("?")[0] ?? pattern;
|
|
2159
|
+
const escaped = path13.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
2147
2160
|
return new RegExp(`^${escaped}$`);
|
|
2148
2161
|
}
|
|
2149
2162
|
function suitePathEqual(a, b) {
|
|
@@ -2166,13 +2179,13 @@ function matchProbeItem(plan, test) {
|
|
|
2166
2179
|
function requestMatchesRoute(method, url, routeKey2) {
|
|
2167
2180
|
const { method: rm, pattern } = parseRouteKey(routeKey2);
|
|
2168
2181
|
if (rm && rm !== method.toUpperCase()) return false;
|
|
2169
|
-
let
|
|
2182
|
+
let path13;
|
|
2170
2183
|
try {
|
|
2171
|
-
|
|
2184
|
+
path13 = new URL(url).pathname;
|
|
2172
2185
|
} catch {
|
|
2173
|
-
|
|
2186
|
+
path13 = url.split("?")[0] ?? url;
|
|
2174
2187
|
}
|
|
2175
|
-
return patternToRegex(pattern).test(
|
|
2188
|
+
return patternToRegex(pattern).test(path13);
|
|
2176
2189
|
}
|
|
2177
2190
|
function createProbeState() {
|
|
2178
2191
|
return { navigated: false, matches: 0 };
|
|
@@ -2194,10 +2207,10 @@ function isProbeMode() {
|
|
|
2194
2207
|
function loadPlan() {
|
|
2195
2208
|
if (planCache !== void 0) return planCache;
|
|
2196
2209
|
planCache = null;
|
|
2197
|
-
const
|
|
2198
|
-
if (
|
|
2210
|
+
const path13 = process.env[PIWI_PROBE_ENV.plan];
|
|
2211
|
+
if (path13) {
|
|
2199
2212
|
try {
|
|
2200
|
-
planCache = JSON.parse(fs2.readFileSync(
|
|
2213
|
+
planCache = JSON.parse(fs2.readFileSync(path13, "utf8"));
|
|
2201
2214
|
} catch {
|
|
2202
2215
|
planCache = null;
|
|
2203
2216
|
}
|
|
@@ -2219,10 +2232,10 @@ function outcomeFromStatus(status, applied) {
|
|
|
2219
2232
|
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
2220
2233
|
}
|
|
2221
2234
|
function recordProbeOutcome(line) {
|
|
2222
|
-
const
|
|
2223
|
-
if (!
|
|
2235
|
+
const path13 = process.env[PIWI_PROBE_ENV.results];
|
|
2236
|
+
if (!path13) return;
|
|
2224
2237
|
try {
|
|
2225
|
-
fs2.appendFileSync(
|
|
2238
|
+
fs2.appendFileSync(path13, `${JSON.stringify(line)}
|
|
2226
2239
|
`);
|
|
2227
2240
|
} catch {
|
|
2228
2241
|
}
|
|
@@ -3496,12 +3509,23 @@ var BOXED_PREFIX_EXPORTS = [
|
|
|
3496
3509
|
["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
|
|
3497
3510
|
];
|
|
3498
3511
|
var boxing;
|
|
3512
|
+
var resolvedPlaywrightPackageJson;
|
|
3513
|
+
function resolvePlaywrightPackageJson() {
|
|
3514
|
+
if (resolvedPlaywrightPackageJson !== void 0) return resolvedPlaywrightPackageJson;
|
|
3515
|
+
try {
|
|
3516
|
+
const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
|
|
3517
|
+
resolvedPlaywrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
|
|
3518
|
+
} catch {
|
|
3519
|
+
resolvedPlaywrightPackageJson = null;
|
|
3520
|
+
}
|
|
3521
|
+
return resolvedPlaywrightPackageJson;
|
|
3522
|
+
}
|
|
3499
3523
|
function resolveBoxing() {
|
|
3500
3524
|
try {
|
|
3501
3525
|
const root = findOwnPackageJson(__dirname)?.root;
|
|
3502
3526
|
if (!root) return null;
|
|
3503
|
-
const
|
|
3504
|
-
|
|
3527
|
+
const playwrightPackageJson = resolvePlaywrightPackageJson();
|
|
3528
|
+
if (!playwrightPackageJson) return null;
|
|
3505
3529
|
const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
|
|
3506
3530
|
for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
|
|
3507
3531
|
let set;
|
|
@@ -3823,9 +3847,9 @@ function resolveSourcePath(source, roots) {
|
|
|
3823
3847
|
const normalized = normalizeSourcePath(source);
|
|
3824
3848
|
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
3825
3849
|
if (path9.isAbsolute(normalized) && isFile(normalized)) return path9.resolve(normalized);
|
|
3826
|
-
const
|
|
3850
|
+
const relative8 = normalized.replace(/^\/+/, "");
|
|
3827
3851
|
for (const root of roots) {
|
|
3828
|
-
const candidate = path9.resolve(root,
|
|
3852
|
+
const candidate = path9.resolve(root, relative8);
|
|
3829
3853
|
if (isFile(candidate)) return candidate;
|
|
3830
3854
|
}
|
|
3831
3855
|
return null;
|
|
@@ -3975,6 +3999,871 @@ async function stopCodeReach(page) {
|
|
|
3975
3999
|
}
|
|
3976
4000
|
}
|
|
3977
4001
|
|
|
4002
|
+
// src/internal/capture/resource-ledger.ts
|
|
4003
|
+
var fs8 = __toESM(require("fs"));
|
|
4004
|
+
var path11 = __toESM(require("path"));
|
|
4005
|
+
var import_node_module2 = require("module");
|
|
4006
|
+
|
|
4007
|
+
// src/internal/capture/worker-metrics.ts
|
|
4008
|
+
var import_node_perf_hooks = require("perf_hooks");
|
|
4009
|
+
|
|
4010
|
+
// src/internal/support/system-readers.ts
|
|
4011
|
+
var fs7 = __toESM(require("fs"));
|
|
4012
|
+
var path10 = __toESM(require("path"));
|
|
4013
|
+
var CLK_TCK = 100;
|
|
4014
|
+
function readText(file) {
|
|
4015
|
+
try {
|
|
4016
|
+
return fs7.readFileSync(file, "utf8");
|
|
4017
|
+
} catch {
|
|
4018
|
+
return null;
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
function parseProcStat(pid, text) {
|
|
4022
|
+
const open = text.indexOf("(");
|
|
4023
|
+
const close = text.lastIndexOf(")");
|
|
4024
|
+
if (open < 0 || close < open) return null;
|
|
4025
|
+
const fields = text.slice(close + 2).trim().split(/\s+/);
|
|
4026
|
+
const ppid = Number(fields[1]);
|
|
4027
|
+
const utime = Number(fields[11]);
|
|
4028
|
+
const stime = Number(fields[12]);
|
|
4029
|
+
const start = Number(fields[19]);
|
|
4030
|
+
if (![ppid, utime, stime, start].every(Number.isFinite)) return null;
|
|
4031
|
+
return { pid, ppid, comm: text.slice(open + 1, close), cpuMs: (utime + stime) * 1e3 / CLK_TCK, start };
|
|
4032
|
+
}
|
|
4033
|
+
function parseSchedstat(text) {
|
|
4034
|
+
const waitNs = Number(text.trim().split(/\s+/)[1]);
|
|
4035
|
+
return Number.isFinite(waitNs) ? waitNs / 1e6 : null;
|
|
4036
|
+
}
|
|
4037
|
+
function parseKbField(text, key) {
|
|
4038
|
+
const match = new RegExp(`^${key}:\\s+(\\d+)`, "m").exec(text);
|
|
4039
|
+
return match ? Number(match[1]) : null;
|
|
4040
|
+
}
|
|
4041
|
+
function parseCpuTicks(text) {
|
|
4042
|
+
const line = text.split("\n", 1)[0] ?? "";
|
|
4043
|
+
if (!line.startsWith("cpu ")) return null;
|
|
4044
|
+
const v = line.trim().split(/\s+/).slice(1, 9).map(Number);
|
|
4045
|
+
if (v.length < 8 || v.some((n) => !Number.isFinite(n))) return null;
|
|
4046
|
+
const [user, nice, system, idle, iowait, irq, softirq, steal] = v;
|
|
4047
|
+
return { busy: user + nice + system + irq + softirq, idle, iowait, steal, total: v.reduce((sum, n) => sum + n, 0) };
|
|
4048
|
+
}
|
|
4049
|
+
function parsePsiTotal(text) {
|
|
4050
|
+
const match = /^some\b.*\btotal=(\d+)/m.exec(text);
|
|
4051
|
+
return match ? Number(match[1]) : null;
|
|
4052
|
+
}
|
|
4053
|
+
function roleOf(comm, cmdline) {
|
|
4054
|
+
const command = `${comm} ${cmdline}`;
|
|
4055
|
+
if (/workerProcessEntry/.test(cmdline)) return "worker";
|
|
4056
|
+
if (/loaderProcessEntry/.test(cmdline)) return "runner";
|
|
4057
|
+
if (/(^|[\s/])ffmpeg/.test(command)) return "ffmpeg";
|
|
4058
|
+
const type = /--type=([\w-]+)/.exec(cmdline)?.[1];
|
|
4059
|
+
if (/chrom|headless_shell|msedge|Google Chrome/i.test(command)) {
|
|
4060
|
+
if (!type) return "browser";
|
|
4061
|
+
if (type === "renderer") return "renderer";
|
|
4062
|
+
if (type === "gpu-process") return "gpu";
|
|
4063
|
+
return "utility";
|
|
4064
|
+
}
|
|
4065
|
+
if (/firefox/i.test(command)) return /-contentproc/.test(cmdline) ? "renderer" : "browser";
|
|
4066
|
+
if (/WebKitWebProcess/.test(command)) return "renderer";
|
|
4067
|
+
if (/WebKitGPUProcess/.test(command)) return "gpu";
|
|
4068
|
+
if (/WebKitNetworkProcess/.test(command)) return "utility";
|
|
4069
|
+
if (/MiniBrowser|pw_run\.sh/.test(command)) return "browser";
|
|
4070
|
+
return null;
|
|
4071
|
+
}
|
|
4072
|
+
function descendantsOf(root, table) {
|
|
4073
|
+
const children = /* @__PURE__ */ new Map();
|
|
4074
|
+
for (const proc of table) {
|
|
4075
|
+
const list = children.get(proc.ppid) ?? [];
|
|
4076
|
+
list.push(proc);
|
|
4077
|
+
children.set(proc.ppid, list);
|
|
4078
|
+
}
|
|
4079
|
+
const out = [];
|
|
4080
|
+
const stack = [root];
|
|
4081
|
+
const seen = /* @__PURE__ */ new Set([root]);
|
|
4082
|
+
while (stack.length > 0) {
|
|
4083
|
+
for (const child of children.get(stack.pop()) ?? []) {
|
|
4084
|
+
if (seen.has(child.pid)) continue;
|
|
4085
|
+
seen.add(child.pid);
|
|
4086
|
+
out.push(child);
|
|
4087
|
+
stack.push(child.pid);
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
return out;
|
|
4091
|
+
}
|
|
4092
|
+
var ProcFs = class {
|
|
4093
|
+
constructor(root = "/proc") {
|
|
4094
|
+
this.root = root;
|
|
4095
|
+
}
|
|
4096
|
+
/** Whether this looks like a Linux `/proc` at all. */
|
|
4097
|
+
available() {
|
|
4098
|
+
return fs7.existsSync(path10.join(this.root, "stat"));
|
|
4099
|
+
}
|
|
4100
|
+
/** Every process, keyed by pid. */
|
|
4101
|
+
table() {
|
|
4102
|
+
const out = /* @__PURE__ */ new Map();
|
|
4103
|
+
let names;
|
|
4104
|
+
try {
|
|
4105
|
+
names = fs7.readdirSync(this.root);
|
|
4106
|
+
} catch {
|
|
4107
|
+
return out;
|
|
4108
|
+
}
|
|
4109
|
+
for (const name of names) {
|
|
4110
|
+
if (!/^\d+$/.test(name)) continue;
|
|
4111
|
+
const text = readText(path10.join(this.root, name, "stat"));
|
|
4112
|
+
const stat = text ? parseProcStat(Number(name), text) : null;
|
|
4113
|
+
if (stat) out.set(stat.pid, stat);
|
|
4114
|
+
}
|
|
4115
|
+
return out;
|
|
4116
|
+
}
|
|
4117
|
+
stat(pid) {
|
|
4118
|
+
const text = readText(path10.join(this.root, String(pid), "stat"));
|
|
4119
|
+
return text ? parseProcStat(pid, text) : null;
|
|
4120
|
+
}
|
|
4121
|
+
runWaitMs(pid) {
|
|
4122
|
+
const text = readText(path10.join(this.root, String(pid), "schedstat"));
|
|
4123
|
+
return text ? parseSchedstat(text) : null;
|
|
4124
|
+
}
|
|
4125
|
+
cmdline(pid) {
|
|
4126
|
+
return (readText(path10.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
|
|
4127
|
+
}
|
|
4128
|
+
/** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
|
|
4129
|
+
statusKb(pid, key) {
|
|
4130
|
+
const text = readText(path10.join(this.root, String(pid), "status"));
|
|
4131
|
+
return text ? parseKbField(text, key) : null;
|
|
4132
|
+
}
|
|
4133
|
+
/** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
|
|
4134
|
+
async pssKb(pid) {
|
|
4135
|
+
try {
|
|
4136
|
+
return parseKbField(await fs7.promises.readFile(path10.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
|
|
4137
|
+
} catch {
|
|
4138
|
+
return null;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
/** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
|
|
4142
|
+
resetPeakRss(pid) {
|
|
4143
|
+
try {
|
|
4144
|
+
fs7.writeFileSync(path10.join(this.root, String(pid), "clear_refs"), "5");
|
|
4145
|
+
return true;
|
|
4146
|
+
} catch {
|
|
4147
|
+
return false;
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
cpuTicks() {
|
|
4151
|
+
const text = readText(path10.join(this.root, "stat"));
|
|
4152
|
+
return text ? parseCpuTicks(text) : null;
|
|
4153
|
+
}
|
|
4154
|
+
/** Total stall time of a resource's pressure file, in µs. */
|
|
4155
|
+
pressureUs(resource) {
|
|
4156
|
+
const text = readText(path10.join(this.root, "pressure", resource));
|
|
4157
|
+
return text ? parsePsiTotal(text) : null;
|
|
4158
|
+
}
|
|
4159
|
+
memAvailableKb() {
|
|
4160
|
+
const text = readText(path10.join(this.root, "meminfo"));
|
|
4161
|
+
return text ? parseKbField(text, "MemAvailable") : null;
|
|
4162
|
+
}
|
|
4163
|
+
/** Open file descriptors of a process. */
|
|
4164
|
+
fdCount(pid) {
|
|
4165
|
+
try {
|
|
4166
|
+
return fs7.readdirSync(path10.join(this.root, String(pid), "fd")).length;
|
|
4167
|
+
} catch {
|
|
4168
|
+
return null;
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
};
|
|
4172
|
+
var UNLIMITED = 2 ** 60;
|
|
4173
|
+
|
|
4174
|
+
// src/internal/capture/worker-metrics.ts
|
|
4175
|
+
var MB = 1024 * 1024;
|
|
4176
|
+
function round1(value) {
|
|
4177
|
+
return Math.round(value * 10) / 10;
|
|
4178
|
+
}
|
|
4179
|
+
var WorkerMetrics = class {
|
|
4180
|
+
constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
|
|
4181
|
+
this.proc = proc;
|
|
4182
|
+
this.pid = pid;
|
|
4183
|
+
this.loop = null;
|
|
4184
|
+
this.usage = null;
|
|
4185
|
+
/** Each process's CPU and wait at the start of the current window; null before the first. */
|
|
4186
|
+
this.subtree = null;
|
|
4187
|
+
this.roles = /* @__PURE__ */ new Map();
|
|
4188
|
+
this.linux = platform === "linux" && proc.available();
|
|
4189
|
+
let histogram = null;
|
|
4190
|
+
try {
|
|
4191
|
+
histogram = (0, import_node_perf_hooks.monitorEventLoopDelay)({ resolution: 20 });
|
|
4192
|
+
histogram.enable();
|
|
4193
|
+
} catch {
|
|
4194
|
+
histogram = null;
|
|
4195
|
+
}
|
|
4196
|
+
this.histogram = histogram;
|
|
4197
|
+
}
|
|
4198
|
+
/** The worker's processes under it, keyed by pid and start time. */
|
|
4199
|
+
tree() {
|
|
4200
|
+
const table = this.proc.table();
|
|
4201
|
+
return descendantsOf(this.pid, table.values()).map((stat) => {
|
|
4202
|
+
const key = `${stat.pid}:${stat.start}`;
|
|
4203
|
+
let role = this.roles.get(key);
|
|
4204
|
+
if (!role) {
|
|
4205
|
+
role = roleOf(stat.comm, this.proc.cmdline(stat.pid)) ?? "other";
|
|
4206
|
+
this.roles.set(key, role);
|
|
4207
|
+
}
|
|
4208
|
+
return { key, pid: stat.pid, cpuMs: stat.cpuMs, role };
|
|
4209
|
+
});
|
|
4210
|
+
}
|
|
4211
|
+
/**
|
|
4212
|
+
* The processes under the worker now, and the reset of their peak memory: the
|
|
4213
|
+
* start of the next window. Taken once per test, at its end, so the time
|
|
4214
|
+
* between two tests counts toward the next one.
|
|
4215
|
+
*/
|
|
4216
|
+
snapshot() {
|
|
4217
|
+
this.subtree = /* @__PURE__ */ new Map();
|
|
4218
|
+
for (const proc of this.tree()) {
|
|
4219
|
+
this.subtree.set(proc.key, { cpuMs: proc.cpuMs, waitMs: this.proc.runWaitMs(proc.pid) });
|
|
4220
|
+
this.proc.resetPeakRss(proc.pid);
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
/**
|
|
4224
|
+
* Start a test's window. `processes` says whether the worker has started any
|
|
4225
|
+
* browser to read; a worker that never did skips the process tree.
|
|
4226
|
+
*/
|
|
4227
|
+
start(processes = true) {
|
|
4228
|
+
try {
|
|
4229
|
+
this.histogram?.reset();
|
|
4230
|
+
this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
|
|
4231
|
+
this.usage = process.resourceUsage();
|
|
4232
|
+
if (this.linux && processes && !this.subtree) this.snapshot();
|
|
4233
|
+
} catch {
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
/** Close the window. Null when the test's start was never read. */
|
|
4237
|
+
end(processes = true) {
|
|
4238
|
+
if (!this.usage || !this.loop) return null;
|
|
4239
|
+
try {
|
|
4240
|
+
const usage = process.resourceUsage();
|
|
4241
|
+
const loop = import_node_perf_hooks.performance.eventLoopUtilization(this.loop);
|
|
4242
|
+
const metrics = {
|
|
4243
|
+
worker: {
|
|
4244
|
+
cpuMs: Math.round(
|
|
4245
|
+
(usage.userCPUTime - this.usage.userCPUTime + usage.systemCPUTime - this.usage.systemCPUTime) / 1e3
|
|
4246
|
+
),
|
|
4247
|
+
involuntarySwitches: usage.involuntaryContextSwitches - this.usage.involuntaryContextSwitches,
|
|
4248
|
+
loopUtilization: Math.round(loop.utilization * 1e3) / 1e3,
|
|
4249
|
+
loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
|
|
4250
|
+
loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
|
|
4251
|
+
heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
|
|
4252
|
+
fds: this.linux ? this.proc.fdCount(this.pid) : null
|
|
4253
|
+
}
|
|
4254
|
+
};
|
|
4255
|
+
this.usage = null;
|
|
4256
|
+
this.loop = null;
|
|
4257
|
+
if (this.linux && processes) metrics.roles = this.roleCosts();
|
|
4258
|
+
return metrics;
|
|
4259
|
+
} catch {
|
|
4260
|
+
return null;
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
/** The window's cost by role; what it reads becomes the start of the next window. */
|
|
4264
|
+
roleCosts() {
|
|
4265
|
+
const roles = {};
|
|
4266
|
+
const start = this.subtree ?? /* @__PURE__ */ new Map();
|
|
4267
|
+
const next = /* @__PURE__ */ new Map();
|
|
4268
|
+
for (const proc of this.tree()) {
|
|
4269
|
+
const from = start.get(proc.key) ?? { cpuMs: 0, waitMs: 0 };
|
|
4270
|
+
const waitMs = this.proc.runWaitMs(proc.pid);
|
|
4271
|
+
const peakKb = this.proc.statusKb(proc.pid, "VmHWM");
|
|
4272
|
+
const cost = roles[proc.role] ??= { cpuMs: 0, runWaitMs: null, peakRssMb: null, processes: 0 };
|
|
4273
|
+
cost.cpuMs += Math.max(0, proc.cpuMs - from.cpuMs);
|
|
4274
|
+
if (waitMs !== null && from.waitMs !== null)
|
|
4275
|
+
cost.runWaitMs = round1((cost.runWaitMs ?? 0) + Math.max(0, waitMs - from.waitMs));
|
|
4276
|
+
if (peakKb !== null) cost.peakRssMb = Math.max(cost.peakRssMb ?? 0, round1(peakKb / 1024));
|
|
4277
|
+
cost.processes++;
|
|
4278
|
+
next.set(proc.key, { cpuMs: proc.cpuMs, waitMs });
|
|
4279
|
+
this.proc.resetPeakRss(proc.pid);
|
|
4280
|
+
}
|
|
4281
|
+
this.subtree = next;
|
|
4282
|
+
return roles;
|
|
4283
|
+
}
|
|
4284
|
+
dispose() {
|
|
4285
|
+
this.histogram?.disable();
|
|
4286
|
+
}
|
|
4287
|
+
};
|
|
4288
|
+
var PageReader = class {
|
|
4289
|
+
constructor() {
|
|
4290
|
+
this.sessions = /* @__PURE__ */ new WeakMap();
|
|
4291
|
+
}
|
|
4292
|
+
async read(page) {
|
|
4293
|
+
let session = this.sessions.get(page);
|
|
4294
|
+
if (!session) {
|
|
4295
|
+
session = internalCall(page, async () => {
|
|
4296
|
+
const opened2 = await page.context().newCDPSession(page);
|
|
4297
|
+
await opened2.send("Performance.enable", { timeDomain: "threadTicks" });
|
|
4298
|
+
return opened2;
|
|
4299
|
+
}).catch(() => null);
|
|
4300
|
+
this.sessions.set(page, session);
|
|
4301
|
+
}
|
|
4302
|
+
const opened = await session;
|
|
4303
|
+
if (!opened) return null;
|
|
4304
|
+
try {
|
|
4305
|
+
const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
|
|
4306
|
+
const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
|
|
4307
|
+
return {
|
|
4308
|
+
cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
|
|
4309
|
+
heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
|
|
4310
|
+
nodes: values.get("Nodes") ?? 0,
|
|
4311
|
+
listeners: values.get("JSEventListeners") ?? 0
|
|
4312
|
+
};
|
|
4313
|
+
} catch {
|
|
4314
|
+
return null;
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
};
|
|
4318
|
+
|
|
4319
|
+
// src/internal/capture/resource-ledger.ts
|
|
4320
|
+
var TRACKED_HANDLE_TYPES = ["TCPServerWrap", "FSEventWrap", "StatWatcher"];
|
|
4321
|
+
var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
|
|
4322
|
+
var CLOSE_TIMEOUT_MS = 5e3;
|
|
4323
|
+
var PAGE_READ_CAP = 50;
|
|
4324
|
+
var PAGE_READ_TIMEOUT_MS = 1e3;
|
|
4325
|
+
var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
|
|
4326
|
+
var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
|
|
4327
|
+
function relativePath(file) {
|
|
4328
|
+
return path11.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path11.sep).join("/");
|
|
4329
|
+
}
|
|
4330
|
+
function isLibraryFile(file) {
|
|
4331
|
+
return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path11.sep}node_modules${path11.sep}`) || file.includes("/node_modules/") || file === __filename;
|
|
4332
|
+
}
|
|
4333
|
+
function userSite(stack) {
|
|
4334
|
+
for (const line of (stack ?? "").split("\n").slice(1)) {
|
|
4335
|
+
const match = /^\s*at\s+(?:.*?\s+\()?(.+?):(\d+):\d+\)?$/.exec(line);
|
|
4336
|
+
if (!match) continue;
|
|
4337
|
+
const file = match[1].replace(/^file:\/\//, "");
|
|
4338
|
+
if (isLibraryFile(file) || !/\.[a-z]+$/i.test(file)) continue;
|
|
4339
|
+
return `${relativePath(file)}:${match[2]}`;
|
|
4340
|
+
}
|
|
4341
|
+
return null;
|
|
4342
|
+
}
|
|
4343
|
+
function zoneSite(zone) {
|
|
4344
|
+
for (const frame of zone?.frames ?? []) {
|
|
4345
|
+
if (!frame?.file || isLibraryFile(frame.file)) continue;
|
|
4346
|
+
return `${relativePath(frame.file)}:${frame.line ?? 0}`;
|
|
4347
|
+
}
|
|
4348
|
+
return null;
|
|
4349
|
+
}
|
|
4350
|
+
function testRef2(info) {
|
|
4351
|
+
if (!info?.testId) return null;
|
|
4352
|
+
const titlePath = Array.isArray(info.titlePath) ? info.titlePath : [];
|
|
4353
|
+
return {
|
|
4354
|
+
id: info.testId,
|
|
4355
|
+
file: info.file ? relativePath(info.file) : "",
|
|
4356
|
+
suite: titlePath.slice(1, -1).map(String)
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
function runnableOf(info) {
|
|
4360
|
+
if (!info) return { phase: "worker", fixture: null };
|
|
4361
|
+
const runnable = info._timeoutManager?._running?.runnable;
|
|
4362
|
+
const type = typeof runnable?.type === "string" ? runnable.type : "test";
|
|
4363
|
+
const phase = HOOK_PHASES.has(type) ? type : type === "teardown" ? "worker" : "test";
|
|
4364
|
+
const raw = runnable?.fixture;
|
|
4365
|
+
const fixture = raw && typeof raw.title === "string" ? {
|
|
4366
|
+
title: raw.title,
|
|
4367
|
+
location: raw.location?.file ? `${relativePath(raw.location.file)}:${raw.location.line ?? 0}` : null,
|
|
4368
|
+
worker: raw.slot !== void 0 && raw.slot !== null
|
|
4369
|
+
} : null;
|
|
4370
|
+
return { phase, fixture };
|
|
4371
|
+
}
|
|
4372
|
+
function fallbackRunnable(window) {
|
|
4373
|
+
return { phase: window ? "test" : "worker", fixture: null };
|
|
4374
|
+
}
|
|
4375
|
+
function playwrightTestInfoSource() {
|
|
4376
|
+
try {
|
|
4377
|
+
const packageJson = resolvePlaywrightPackageJson();
|
|
4378
|
+
if (!packageJson) return () => null;
|
|
4379
|
+
const globals = (0, import_node_module2.createRequire)(packageJson)(path11.join(path11.dirname(packageJson), "lib", "globals.js"));
|
|
4380
|
+
const read = globals.currentTestInfo;
|
|
4381
|
+
if (typeof read !== "function") return () => null;
|
|
4382
|
+
return () => {
|
|
4383
|
+
try {
|
|
4384
|
+
return read() ?? null;
|
|
4385
|
+
} catch {
|
|
4386
|
+
return null;
|
|
4387
|
+
}
|
|
4388
|
+
};
|
|
4389
|
+
} catch {
|
|
4390
|
+
return () => null;
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
|
|
4394
|
+
return value === "fail" || value === "close" ? value : "report";
|
|
4395
|
+
}
|
|
4396
|
+
function countHandles() {
|
|
4397
|
+
const counts = {};
|
|
4398
|
+
const read = process.getActiveResourcesInfo;
|
|
4399
|
+
if (typeof read !== "function") return counts;
|
|
4400
|
+
for (const type of read.call(process)) {
|
|
4401
|
+
if (TRACKED_HANDLE_TYPES.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
|
|
4402
|
+
}
|
|
4403
|
+
return counts;
|
|
4404
|
+
}
|
|
4405
|
+
async function settledHandles(start) {
|
|
4406
|
+
const end = countHandles();
|
|
4407
|
+
if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
|
|
4408
|
+
await new Promise((resolve3) => setTimeout(resolve3, 0));
|
|
4409
|
+
return countHandles();
|
|
4410
|
+
}
|
|
4411
|
+
function settleWithin(promise, ms) {
|
|
4412
|
+
let timer;
|
|
4413
|
+
return Promise.race([
|
|
4414
|
+
promise.catch(() => void 0),
|
|
4415
|
+
new Promise((resolve3) => {
|
|
4416
|
+
timer = setTimeout(resolve3, ms);
|
|
4417
|
+
})
|
|
4418
|
+
]).finally(() => clearTimeout(timer));
|
|
4419
|
+
}
|
|
4420
|
+
var ResourceLedger = class {
|
|
4421
|
+
constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}) {
|
|
4422
|
+
this.playwright = playwright;
|
|
4423
|
+
this.workerIndex = workerIndex;
|
|
4424
|
+
this.leakCheck = leakCheck;
|
|
4425
|
+
this.resultsFile = resultsFile;
|
|
4426
|
+
this.testInfoSource = testInfoSource;
|
|
4427
|
+
this.now = now;
|
|
4428
|
+
this.readers = readers;
|
|
4429
|
+
this.nextId = 1;
|
|
4430
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
4431
|
+
this.byObject = /* @__PURE__ */ new WeakMap();
|
|
4432
|
+
this.pendingCalls = /* @__PURE__ */ new Set();
|
|
4433
|
+
this.lastCall = null;
|
|
4434
|
+
/** The page a wrapped page method is calling into: its first reported API call marks it used. */
|
|
4435
|
+
this.usingPage = null;
|
|
4436
|
+
this.born = [];
|
|
4437
|
+
this.closed = [];
|
|
4438
|
+
this.handlesAtStart = {};
|
|
4439
|
+
this.openAtStart = { contexts: 0, pages: 0 };
|
|
4440
|
+
/** The test between `testStarted` and `testEnded`, for when Playwright's own test info cannot be read. */
|
|
4441
|
+
this.window = null;
|
|
4442
|
+
this.listener = null;
|
|
4443
|
+
/** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
|
|
4444
|
+
this.sawBrowser = false;
|
|
4445
|
+
}
|
|
4446
|
+
/** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
|
|
4447
|
+
install() {
|
|
4448
|
+
const instrumentation = this.playwright?._instrumentation;
|
|
4449
|
+
if (!instrumentation || typeof instrumentation.addListener !== "function") return false;
|
|
4450
|
+
this.listener = {
|
|
4451
|
+
onApiCallBegin: (zone) => {
|
|
4452
|
+
if (this.pendingCalls.size > 1e3) this.pendingCalls.clear();
|
|
4453
|
+
this.pendingCalls.add(zone);
|
|
4454
|
+
this.lastCall = zone;
|
|
4455
|
+
if (this.usingPage) {
|
|
4456
|
+
this.usingPage.used = true;
|
|
4457
|
+
this.usingPage = null;
|
|
4458
|
+
}
|
|
4459
|
+
},
|
|
4460
|
+
onApiCallEnd: (zone) => {
|
|
4461
|
+
this.pendingCalls.delete(zone);
|
|
4462
|
+
},
|
|
4463
|
+
runAfterCreateBrowserContext: async (context) => {
|
|
4464
|
+
this.guard(() => this.onContext(context, userSite(new Error().stack)));
|
|
4465
|
+
},
|
|
4466
|
+
runAfterCreateRequestContext: async (request) => {
|
|
4467
|
+
this.guard(() => this.track(request, "request", null, userSite(new Error().stack)));
|
|
4468
|
+
},
|
|
4469
|
+
runBeforeCloseRequestContext: async (request) => {
|
|
4470
|
+
this.guard(() => this.onClosed(request));
|
|
4471
|
+
}
|
|
4472
|
+
};
|
|
4473
|
+
instrumentation.addListener(this.listener);
|
|
4474
|
+
for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
|
|
4475
|
+
if (type) this.wrapBrowserType(type);
|
|
4476
|
+
}
|
|
4477
|
+
return true;
|
|
4478
|
+
}
|
|
4479
|
+
/** Stop listening; the objects already tracked stay in the ledger. */
|
|
4480
|
+
uninstall() {
|
|
4481
|
+
const remove = this.playwright?._instrumentation?.removeListener;
|
|
4482
|
+
if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
|
|
4483
|
+
this.listener = null;
|
|
4484
|
+
}
|
|
4485
|
+
/** Mark a page used: a locator was built on it. */
|
|
4486
|
+
notePageUse(page) {
|
|
4487
|
+
const entry = this.byObject.get(page);
|
|
4488
|
+
if (entry) entry.used = true;
|
|
4489
|
+
}
|
|
4490
|
+
/** Open the census window of a test. */
|
|
4491
|
+
testStarted(info) {
|
|
4492
|
+
this.window = testRef2(info);
|
|
4493
|
+
this.handlesAtStart = countHandles();
|
|
4494
|
+
this.openAtStart = { contexts: 0, pages: 0 };
|
|
4495
|
+
for (const entry of this.openEntries()) {
|
|
4496
|
+
if (entry.birth.kind === "page") this.openAtStart.pages++;
|
|
4497
|
+
else if (entry.birth.kind === "context") this.openAtStart.contexts++;
|
|
4498
|
+
}
|
|
4499
|
+
this.readers.metrics?.start(this.sawBrowser);
|
|
4500
|
+
}
|
|
4501
|
+
/**
|
|
4502
|
+
* Close the census window of a test: apply `PIWI_LEAK_CHECK=close` to what the
|
|
4503
|
+
* test left open, then take the census. Called after the test-scoped fixtures
|
|
4504
|
+
* tore down, so what the test opened through them is closed by now.
|
|
4505
|
+
*/
|
|
4506
|
+
async testEnded(info) {
|
|
4507
|
+
const test = testRef2(info) ?? this.window;
|
|
4508
|
+
const failed = typeof info.status === "string" && typeof info.expectedStatus === "string" && info.status !== info.expectedStatus;
|
|
4509
|
+
const leaks = test && !failed ? this.leaksOf(test.id) : [];
|
|
4510
|
+
if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
|
|
4511
|
+
const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
|
|
4512
|
+
const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
|
|
4513
|
+
const handles = { start: this.handlesAtStart, end: await settledHandles(this.handlesAtStart) };
|
|
4514
|
+
const census = this.census(ref, handles, await this.readPages());
|
|
4515
|
+
if (metrics) census.metrics = metrics;
|
|
4516
|
+
if (test) {
|
|
4517
|
+
census.openAtStart = this.openAtStart;
|
|
4518
|
+
if (!failed) census.leftOpen = leaks.length;
|
|
4519
|
+
}
|
|
4520
|
+
this.window = null;
|
|
4521
|
+
return { census, leaks };
|
|
4522
|
+
}
|
|
4523
|
+
/** The open pages' main thread and weight, over CDP, within a deadline. */
|
|
4524
|
+
async readPages() {
|
|
4525
|
+
const out = /* @__PURE__ */ new Map();
|
|
4526
|
+
const reader = this.readers.pages;
|
|
4527
|
+
if (!reader) return out;
|
|
4528
|
+
const pages = this.openEntries().filter((entry) => entry.birth.kind === "page").slice(0, PAGE_READ_CAP);
|
|
4529
|
+
await settleWithin(
|
|
4530
|
+
Promise.all(
|
|
4531
|
+
pages.map(async (entry) => {
|
|
4532
|
+
const main = entry.object ? await reader.read(entry.object) : null;
|
|
4533
|
+
if (main) out.set(entry.birth.id, main);
|
|
4534
|
+
})
|
|
4535
|
+
),
|
|
4536
|
+
PAGE_READ_TIMEOUT_MS
|
|
4537
|
+
);
|
|
4538
|
+
return out;
|
|
4539
|
+
}
|
|
4540
|
+
/** The census a worker takes as it shuts down, appended to the results file when the reporter set one. */
|
|
4541
|
+
workerEnded() {
|
|
4542
|
+
const census = this.census(null);
|
|
4543
|
+
if (this.resultsFile) {
|
|
4544
|
+
try {
|
|
4545
|
+
fs8.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
|
|
4546
|
+
`);
|
|
4547
|
+
} catch {
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
this.readers.metrics?.dispose();
|
|
4551
|
+
this.uninstall();
|
|
4552
|
+
return census;
|
|
4553
|
+
}
|
|
4554
|
+
guard(run) {
|
|
4555
|
+
try {
|
|
4556
|
+
run();
|
|
4557
|
+
} catch {
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
wrapBrowserType(type) {
|
|
4561
|
+
for (const method of ["launch", "connect", "connectOverCDP"]) {
|
|
4562
|
+
const original = type[method];
|
|
4563
|
+
if (typeof original !== "function") continue;
|
|
4564
|
+
const bound = original.bind(type);
|
|
4565
|
+
type[method] = async (...args) => {
|
|
4566
|
+
const site = userSite(new Error().stack);
|
|
4567
|
+
const browser = await bound(...args);
|
|
4568
|
+
this.guard(() => this.onBrowser(browser, site));
|
|
4569
|
+
return browser;
|
|
4570
|
+
};
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
track(object, kind, parent, site) {
|
|
4574
|
+
const existing = this.byObject.get(object);
|
|
4575
|
+
if (existing) return existing;
|
|
4576
|
+
const info = this.testInfoSource();
|
|
4577
|
+
const { phase, fixture } = info ? runnableOf(info) : fallbackRunnable(this.window);
|
|
4578
|
+
const birth = {
|
|
4579
|
+
id: this.nextId++,
|
|
4580
|
+
kind,
|
|
4581
|
+
parent: parent ? this.byObject.get(parent)?.birth.id ?? null : null,
|
|
4582
|
+
at: this.now(),
|
|
4583
|
+
phase,
|
|
4584
|
+
fixture,
|
|
4585
|
+
test: testRef2(info) ?? this.window,
|
|
4586
|
+
site
|
|
4587
|
+
};
|
|
4588
|
+
const entry = { birth, object, closed: null, used: false };
|
|
4589
|
+
if (kind === "browser" || kind === "context") this.sawBrowser = true;
|
|
4590
|
+
this.entries.set(birth.id, entry);
|
|
4591
|
+
this.byObject.set(object, entry);
|
|
4592
|
+
this.born.push(birth);
|
|
4593
|
+
return entry;
|
|
4594
|
+
}
|
|
4595
|
+
onBrowser(browser, site) {
|
|
4596
|
+
const known = this.byObject.get(browser);
|
|
4597
|
+
if (known) return known;
|
|
4598
|
+
const entry = this.track(browser, "browser", null, site);
|
|
4599
|
+
browser.on("disconnected", () => this.guard(() => this.onClosed(browser)));
|
|
4600
|
+
return entry;
|
|
4601
|
+
}
|
|
4602
|
+
onContext(context, site) {
|
|
4603
|
+
if (this.byObject.has(context)) return;
|
|
4604
|
+
const browser = context.browser();
|
|
4605
|
+
if (browser) this.onBrowser(browser, site);
|
|
4606
|
+
const entry = this.track(context, "context", browser, site);
|
|
4607
|
+
const zone = this.latestPendingCall();
|
|
4608
|
+
if (zone?.apiName?.endsWith(".newPage")) entry.birth.implicit = true;
|
|
4609
|
+
if (context._forReuse) entry.birth.reused = true;
|
|
4610
|
+
context.on("close", () => this.guard(() => this.onClosed(context)));
|
|
4611
|
+
context.on("page", (page) => this.guard(() => this.onPage(page, context)));
|
|
4612
|
+
for (const page of context.pages()) this.onPage(page, context, { site });
|
|
4613
|
+
}
|
|
4614
|
+
/** `initial` is set for a page that came with its context (a persistent context's), which is no popup. */
|
|
4615
|
+
onPage(page, context, initial) {
|
|
4616
|
+
if (this.byObject.has(page)) return;
|
|
4617
|
+
const zone = this.latestPendingCall();
|
|
4618
|
+
const viaNewPage = !!zone?.apiName?.endsWith(".newPage");
|
|
4619
|
+
const popup = !viaNewPage && !initial;
|
|
4620
|
+
const site = viaNewPage ? zoneSite(zone) : initial ? initial.site : zoneSite(this.lastCall);
|
|
4621
|
+
const entry = this.track(page, "page", context, site);
|
|
4622
|
+
if (popup) {
|
|
4623
|
+
const opener = page._opener;
|
|
4624
|
+
const openerEntry = opener ? this.byObject.get(opener) : void 0;
|
|
4625
|
+
if (openerEntry) entry.birth.opener = openerEntry.birth.id;
|
|
4626
|
+
if (this.lastCall?.apiName) entry.birth.trigger = this.lastCall.apiName;
|
|
4627
|
+
}
|
|
4628
|
+
const markUsed = () => {
|
|
4629
|
+
entry.used = true;
|
|
4630
|
+
};
|
|
4631
|
+
page.on("load", markUsed);
|
|
4632
|
+
page.on("domcontentloaded", markUsed);
|
|
4633
|
+
page.on("framenavigated", (frame) => {
|
|
4634
|
+
try {
|
|
4635
|
+
if (frame === page.mainFrame() && frame.url() !== "about:blank") entry.used = true;
|
|
4636
|
+
} catch {
|
|
4637
|
+
}
|
|
4638
|
+
});
|
|
4639
|
+
page.on("close", () => this.guard(() => this.onClosed(page)));
|
|
4640
|
+
const methods = page;
|
|
4641
|
+
for (const method of USE_METHODS) {
|
|
4642
|
+
const original = methods[method];
|
|
4643
|
+
if (typeof original !== "function") continue;
|
|
4644
|
+
const bound = original.bind(page);
|
|
4645
|
+
methods[method] = (...args) => {
|
|
4646
|
+
const previous = this.usingPage;
|
|
4647
|
+
this.usingPage = entry;
|
|
4648
|
+
try {
|
|
4649
|
+
return bound(...args);
|
|
4650
|
+
} finally {
|
|
4651
|
+
this.usingPage = previous;
|
|
4652
|
+
}
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
onClosed(object, byPiwi = false) {
|
|
4657
|
+
const entry = this.byObject.get(object);
|
|
4658
|
+
if (!entry || entry.closed) return;
|
|
4659
|
+
const info = this.testInfoSource();
|
|
4660
|
+
const { phase } = info ? runnableOf(info) : fallbackRunnable(this.window);
|
|
4661
|
+
const close = {
|
|
4662
|
+
id: entry.birth.id,
|
|
4663
|
+
at: this.now(),
|
|
4664
|
+
phase,
|
|
4665
|
+
test: testRef2(info) ?? this.window
|
|
4666
|
+
};
|
|
4667
|
+
if (entry.birth.kind === "page") close.used = entry.used;
|
|
4668
|
+
if (byPiwi) close.byPiwi = true;
|
|
4669
|
+
entry.closed = close;
|
|
4670
|
+
entry.object = null;
|
|
4671
|
+
this.closed.push(close);
|
|
4672
|
+
}
|
|
4673
|
+
latestPendingCall() {
|
|
4674
|
+
let latest = null;
|
|
4675
|
+
for (const zone of this.pendingCalls) latest = zone;
|
|
4676
|
+
return latest;
|
|
4677
|
+
}
|
|
4678
|
+
openEntries() {
|
|
4679
|
+
return [...this.entries.values()].filter((entry) => !entry.closed && entry.object);
|
|
4680
|
+
}
|
|
4681
|
+
/**
|
|
4682
|
+
* Objects a test opened itself, by its body or its each-hooks, and left open.
|
|
4683
|
+
* A page in a leaked context, or a context in a leaked browser, goes with it;
|
|
4684
|
+
* the context `browser.newPage()` opened for its page goes with the page.
|
|
4685
|
+
*/
|
|
4686
|
+
leaksOf(testId) {
|
|
4687
|
+
const open = this.openEntries().filter(
|
|
4688
|
+
(entry) => entry.birth.test?.id === testId && entry.birth.fixture === null && (entry.birth.phase === "test" || entry.birth.phase === "beforeEach" || entry.birth.phase === "afterEach") && !entry.birth.reused
|
|
4689
|
+
);
|
|
4690
|
+
const openIds = new Set(open.map((entry) => entry.birth.id));
|
|
4691
|
+
const foldsIntoLeak = (entry) => {
|
|
4692
|
+
let parentId = entry.birth.parent;
|
|
4693
|
+
while (parentId !== null) {
|
|
4694
|
+
if (!this.isImplicit(parentId)) return openIds.has(parentId);
|
|
4695
|
+
parentId = this.entries.get(parentId)?.birth.parent ?? null;
|
|
4696
|
+
}
|
|
4697
|
+
return false;
|
|
4698
|
+
};
|
|
4699
|
+
return open.filter((entry) => !(entry.birth.kind === "context" && this.isImplicit(entry.birth.id))).filter((entry) => !foldsIntoLeak(entry)).map((entry) => ({
|
|
4700
|
+
id: entry.birth.id,
|
|
4701
|
+
kind: entry.birth.kind,
|
|
4702
|
+
site: entry.birth.site,
|
|
4703
|
+
pages: entry.birth.kind === "page" ? 0 : this.pagesUnder(entry.birth.id),
|
|
4704
|
+
implicit: false,
|
|
4705
|
+
...entry.birth.opener !== void 0 ? { trigger: entry.birth.trigger ?? "a click" } : {}
|
|
4706
|
+
})).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
|
|
4707
|
+
}
|
|
4708
|
+
/** Open pages under a context, or under any context of a browser. */
|
|
4709
|
+
pagesUnder(id) {
|
|
4710
|
+
let count = 0;
|
|
4711
|
+
for (const entry of this.openEntries()) {
|
|
4712
|
+
if (entry.birth.kind !== "page" || entry.birth.parent === null) continue;
|
|
4713
|
+
const context = this.entries.get(entry.birth.parent);
|
|
4714
|
+
if (entry.birth.parent === id || context?.birth.parent === id) count++;
|
|
4715
|
+
}
|
|
4716
|
+
return count;
|
|
4717
|
+
}
|
|
4718
|
+
isImplicit(id) {
|
|
4719
|
+
const entry = this.entries.get(id);
|
|
4720
|
+
if (!entry || entry.birth.kind !== "context") return false;
|
|
4721
|
+
if (entry.birth.implicit) return true;
|
|
4722
|
+
const owner = entry.object ? entry.object._ownerPage : void 0;
|
|
4723
|
+
if (owner) entry.birth.implicit = true;
|
|
4724
|
+
return !!owner;
|
|
4725
|
+
}
|
|
4726
|
+
openPagesOf(contextId) {
|
|
4727
|
+
let count = 0;
|
|
4728
|
+
for (const entry of this.openEntries())
|
|
4729
|
+
if (entry.birth.kind === "page" && entry.birth.parent === contextId) count++;
|
|
4730
|
+
return count;
|
|
4731
|
+
}
|
|
4732
|
+
async closeLeaks(leaks) {
|
|
4733
|
+
for (const leak of leaks) {
|
|
4734
|
+
const entry = this.entries.get(leak.id);
|
|
4735
|
+
const object = entry?.object;
|
|
4736
|
+
if (!entry || entry.closed || !object) continue;
|
|
4737
|
+
const close = leak.kind === "request" ? () => object.dispose() : () => object.close();
|
|
4738
|
+
await settleWithin(internalCall(object, close), CLOSE_TIMEOUT_MS);
|
|
4739
|
+
const closedMeanwhile = entry.closed;
|
|
4740
|
+
if (closedMeanwhile) closedMeanwhile.byPiwi = true;
|
|
4741
|
+
else this.onClosed(object, true);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
census(test, handles, mains = /* @__PURE__ */ new Map()) {
|
|
4745
|
+
const open = [];
|
|
4746
|
+
for (const entry of this.openEntries()) {
|
|
4747
|
+
const object = entry.object;
|
|
4748
|
+
if (entry.birth.kind === "page") {
|
|
4749
|
+
const main = mains.get(entry.birth.id);
|
|
4750
|
+
open.push({
|
|
4751
|
+
id: entry.birth.id,
|
|
4752
|
+
used: entry.used,
|
|
4753
|
+
url: this.pageUrl(object),
|
|
4754
|
+
listeners: this.listenerCount(object),
|
|
4755
|
+
routes: this.routeCount(object),
|
|
4756
|
+
...main ? { main } : {}
|
|
4757
|
+
});
|
|
4758
|
+
} else if (entry.birth.kind === "context") {
|
|
4759
|
+
this.isImplicit(entry.birth.id);
|
|
4760
|
+
open.push({ id: entry.birth.id, pages: this.openPagesOf(entry.birth.id), routes: this.routeCount(object) });
|
|
4761
|
+
} else {
|
|
4762
|
+
open.push({ id: entry.birth.id });
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
const census = {
|
|
4766
|
+
v: 1,
|
|
4767
|
+
worker: this.workerIndex,
|
|
4768
|
+
pid: process.pid,
|
|
4769
|
+
at: this.now(),
|
|
4770
|
+
test,
|
|
4771
|
+
born: this.born,
|
|
4772
|
+
closed: this.closed,
|
|
4773
|
+
open
|
|
4774
|
+
};
|
|
4775
|
+
if (handles) census.handles = handles;
|
|
4776
|
+
this.born = [];
|
|
4777
|
+
this.closed = [];
|
|
4778
|
+
return census;
|
|
4779
|
+
}
|
|
4780
|
+
pageUrl(page) {
|
|
4781
|
+
try {
|
|
4782
|
+
const url = typeof page.url === "function" ? String(page.url()) : "";
|
|
4783
|
+
return url ? url.split(/[?#]/)[0] : void 0;
|
|
4784
|
+
} catch {
|
|
4785
|
+
return void 0;
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
listenerCount(emitter) {
|
|
4789
|
+
try {
|
|
4790
|
+
if (typeof emitter.eventNames !== "function" || typeof emitter.listenerCount !== "function") return void 0;
|
|
4791
|
+
const names = emitter.eventNames();
|
|
4792
|
+
return names.reduce((sum, name) => sum + emitter.listenerCount(name), 0);
|
|
4793
|
+
} catch {
|
|
4794
|
+
return void 0;
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
routeCount(object) {
|
|
4798
|
+
const routes = object._routes;
|
|
4799
|
+
return Array.isArray(routes) ? routes.length : void 0;
|
|
4800
|
+
}
|
|
4801
|
+
};
|
|
4802
|
+
var activeLedger = null;
|
|
4803
|
+
function startResourceLedger(playwright, workerIndex) {
|
|
4804
|
+
if (process.env[PIWI_ENV_KEYS.captureResources] === "false") return null;
|
|
4805
|
+
if (activeLedger) return activeLedger;
|
|
4806
|
+
const metrics = new WorkerMetrics();
|
|
4807
|
+
const ledger = new ResourceLedger(
|
|
4808
|
+
playwright,
|
|
4809
|
+
workerIndex,
|
|
4810
|
+
readLeakCheck(),
|
|
4811
|
+
process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
|
|
4812
|
+
playwrightTestInfoSource(),
|
|
4813
|
+
Date.now,
|
|
4814
|
+
{ metrics, pages: new PageReader() }
|
|
4815
|
+
);
|
|
4816
|
+
if (!ledger.install()) {
|
|
4817
|
+
metrics.dispose();
|
|
4818
|
+
return null;
|
|
4819
|
+
}
|
|
4820
|
+
activeLedger = ledger;
|
|
4821
|
+
return ledger;
|
|
4822
|
+
}
|
|
4823
|
+
function activeResourceLedger() {
|
|
4824
|
+
return activeLedger;
|
|
4825
|
+
}
|
|
4826
|
+
function stopResourceLedger() {
|
|
4827
|
+
const ledger = activeLedger;
|
|
4828
|
+
activeLedger = null;
|
|
4829
|
+
return ledger ? ledger.workerEnded() : null;
|
|
4830
|
+
}
|
|
4831
|
+
function noteResourceUse(page) {
|
|
4832
|
+
activeLedger?.notePageUse(page);
|
|
4833
|
+
}
|
|
4834
|
+
var ARTICLE = {
|
|
4835
|
+
browser: "a browser",
|
|
4836
|
+
context: "a context",
|
|
4837
|
+
page: "a page",
|
|
4838
|
+
request: "an API request context"
|
|
4839
|
+
};
|
|
4840
|
+
function leakFailureMessage(leaks) {
|
|
4841
|
+
const lines = leaks.map((leak) => {
|
|
4842
|
+
const what = leak.trigger ? `a popup opened by ${leak.trigger}` : ARTICLE[leak.kind];
|
|
4843
|
+
const where = leak.site ? `${leak.trigger ? "" : " opened"} at ${leak.site}` : "";
|
|
4844
|
+
const pages = leak.pages > 0 ? `, with ${leak.pages} page${leak.pages === 1 ? "" : "s"}` : "";
|
|
4845
|
+
return ` - ${what}${where}${pages}`;
|
|
4846
|
+
});
|
|
4847
|
+
return [
|
|
4848
|
+
`Piwi: this test left ${leaks.length === 1 ? "one object" : `${leaks.length} objects`} open:`,
|
|
4849
|
+
...lines,
|
|
4850
|
+
"Close each before the test ends (await using, close() or dispose()), or set PIWI_LEAK_CHECK=report to only list them."
|
|
4851
|
+
].join("\n");
|
|
4852
|
+
}
|
|
4853
|
+
async function recordResourceCensus(testInfo) {
|
|
4854
|
+
const ledger = activeLedger;
|
|
4855
|
+
if (!ledger) return;
|
|
4856
|
+
const { census, leaks } = await ledger.testEnded(testInfo);
|
|
4857
|
+
try {
|
|
4858
|
+
await testInfo.attach(ATTACHMENT_NAMES.resources, {
|
|
4859
|
+
contentType: "application/json",
|
|
4860
|
+
body: Buffer.from(JSON.stringify(census))
|
|
4861
|
+
});
|
|
4862
|
+
} catch {
|
|
4863
|
+
}
|
|
4864
|
+
if (ledger.leakCheck === "fail" && leaks.length > 0) throw new Error(leakFailureMessage(leaks));
|
|
4865
|
+
}
|
|
4866
|
+
|
|
3978
4867
|
// src/internal/capture/capture-fixtures.ts
|
|
3979
4868
|
function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
|
|
3980
4869
|
const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
|
|
@@ -4047,7 +4936,7 @@ function configuredCodeReachRoots() {
|
|
|
4047
4936
|
if (Array.isArray(parsed)) return parsed.filter((r) => typeof r === "string" && r !== "");
|
|
4048
4937
|
} catch {
|
|
4049
4938
|
}
|
|
4050
|
-
return raw.split(
|
|
4939
|
+
return raw.split(path12.delimiter).filter(Boolean);
|
|
4051
4940
|
}
|
|
4052
4941
|
async function stopSinkCodeReach(sink, closing) {
|
|
4053
4942
|
const session = sink.codeReach;
|
|
@@ -4062,7 +4951,7 @@ async function stopSinkCodeReach(sink, closing) {
|
|
|
4062
4951
|
if (!entries) return;
|
|
4063
4952
|
const info = sink.testInfo;
|
|
4064
4953
|
const configFile = info?.config.configFile;
|
|
4065
|
-
const configDir = configFile ?
|
|
4954
|
+
const configDir = configFile ? path12.dirname(configFile) : info?.config.rootDir ?? process.cwd();
|
|
4066
4955
|
session.files = await resolveCodeReach(
|
|
4067
4956
|
entries,
|
|
4068
4957
|
codeReachRoots(configDir, configuredCodeReachRoots()),
|
|
@@ -4626,6 +5515,7 @@ function instrumentPage(page) {
|
|
|
4626
5515
|
const original = factories[method].bind(page);
|
|
4627
5516
|
factories[method] = (...args) => {
|
|
4628
5517
|
if (currentSink) currentSink.lastActivePage = page;
|
|
5518
|
+
noteResourceUse(page);
|
|
4629
5519
|
return wrapLocator(page, original(...args), method, args);
|
|
4630
5520
|
};
|
|
4631
5521
|
}
|
|
@@ -4955,6 +5845,22 @@ async function flushSink(sink, testInfo) {
|
|
|
4955
5845
|
}
|
|
4956
5846
|
}
|
|
4957
5847
|
var piwiFixtures = {
|
|
5848
|
+
// Auto, worker-scoped: the worker's resource ledger. Set up before any hook
|
|
5849
|
+
// (worker fixtures come first), so a `beforeAll` context is in it too, and
|
|
5850
|
+
// torn down after the browser closed, so its census sees what only the
|
|
5851
|
+
// worker's shutdown closed. Depends on `playwright` alone, which launches nothing.
|
|
5852
|
+
piwiResources: [
|
|
5853
|
+
async ({ playwright }, use, workerInfo) => {
|
|
5854
|
+
boxCaptureFrames();
|
|
5855
|
+
startResourceLedger(playwright, workerInfo.workerIndex);
|
|
5856
|
+
try {
|
|
5857
|
+
await use();
|
|
5858
|
+
} finally {
|
|
5859
|
+
stopResourceLedger();
|
|
5860
|
+
}
|
|
5861
|
+
},
|
|
5862
|
+
{ scope: "worker", auto: true }
|
|
5863
|
+
],
|
|
4958
5864
|
// Worker-scoped: patch the shared browser so every page/context created from
|
|
4959
5865
|
// it — including by user fixtures that take `browser` directly — is captured.
|
|
4960
5866
|
browser: [
|
|
@@ -5002,6 +5908,7 @@ var piwiFixtures = {
|
|
|
5002
5908
|
file: testInfo.file,
|
|
5003
5909
|
titlePath: testInfo.titlePath
|
|
5004
5910
|
});
|
|
5911
|
+
activeResourceLedger()?.testStarted(testInfo);
|
|
5005
5912
|
currentSink = sink;
|
|
5006
5913
|
try {
|
|
5007
5914
|
await use();
|
|
@@ -5028,6 +5935,7 @@ var piwiFixtures = {
|
|
|
5028
5935
|
}
|
|
5029
5936
|
if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
|
|
5030
5937
|
await flushSink(sink, testInfo);
|
|
5938
|
+
await recordResourceCensus(testInfo);
|
|
5031
5939
|
}
|
|
5032
5940
|
},
|
|
5033
5941
|
{ auto: true }
|
|
@@ -5061,6 +5969,7 @@ function extendPiwiFixtures(test) {
|
|
|
5061
5969
|
0 && (module.exports = {
|
|
5062
5970
|
CAPTURED_ATTRS_ARG,
|
|
5063
5971
|
LocatorPageLog,
|
|
5972
|
+
activeResourceLedger,
|
|
5064
5973
|
ariaSnapshotBestEffort,
|
|
5065
5974
|
ariaSnapshotJSONBestEffort,
|
|
5066
5975
|
boxCaptureFrames,
|
|
@@ -5073,10 +5982,14 @@ function extendPiwiFixtures(test) {
|
|
|
5073
5982
|
internalCall,
|
|
5074
5983
|
noteLocatorCall,
|
|
5075
5984
|
noteNavigation,
|
|
5985
|
+
noteResourceUse,
|
|
5076
5986
|
pageMapFetcher,
|
|
5077
5987
|
piwiFixtures,
|
|
5078
5988
|
probeElementAttrs,
|
|
5989
|
+
recordResourceCensus,
|
|
5079
5990
|
resolveCodeReach,
|
|
5080
5991
|
startCodeReach,
|
|
5081
|
-
|
|
5992
|
+
startResourceLedger,
|
|
5993
|
+
stopCodeReach,
|
|
5994
|
+
stopResourceLedger
|
|
5082
5995
|
});
|