@piwitests/reporter 0.44.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.
@@ -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
- stopCodeReach: () => stopCodeReach
55
+ startResourceLedger: () => startResourceLedger,
56
+ stopCodeReach: () => stopCodeReach,
57
+ stopResourceLedger: () => stopResourceLedger
53
58
  });
54
59
  module.exports = __toCommonJS(capture_fixtures_exports);
55
- var path10 = __toESM(require("path"));
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 roleOf = (n) => {
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 = roleOf(el);
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 (roleOf(n) !== targetRole) continue;
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 (roleOf(nodes[i]) === role) c++;
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 (roleOf(nodes[i]) !== role) continue;
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 (roleOf(n) !== targetRole) continue;
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 roleOf = (n) => {
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 (roleOf(n) !== leafRole) continue;
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 = roleOf(node);
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 (roleOf(n) !== role) continue;
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) => roleOf(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
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 roleOf = (el) => {
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 = roleOf(el);
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 = roleOf(el);
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 path11 = pattern.split("?")[0] ?? pattern;
2146
- const escaped = path11.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
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 path11;
2182
+ let path13;
2170
2183
  try {
2171
- path11 = new URL(url).pathname;
2184
+ path13 = new URL(url).pathname;
2172
2185
  } catch {
2173
- path11 = url.split("?")[0] ?? url;
2186
+ path13 = url.split("?")[0] ?? url;
2174
2187
  }
2175
- return patternToRegex(pattern).test(path11);
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 path11 = process.env[PIWI_PROBE_ENV.plan];
2198
- if (path11) {
2210
+ const path13 = process.env[PIWI_PROBE_ENV.plan];
2211
+ if (path13) {
2199
2212
  try {
2200
- planCache = JSON.parse(fs2.readFileSync(path11, "utf8"));
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 path11 = process.env[PIWI_PROBE_ENV.results];
2223
- if (!path11) return;
2235
+ const path13 = process.env[PIWI_PROBE_ENV.results];
2236
+ if (!path13) return;
2224
2237
  try {
2225
- fs2.appendFileSync(path11, `${JSON.stringify(line)}
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 testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
3504
- const playwrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
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 relative7 = normalized.replace(/^\/+/, "");
3850
+ const relative8 = normalized.replace(/^\/+/, "");
3827
3851
  for (const root of roots) {
3828
- const candidate = path9.resolve(root, relative7);
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(path10.delimiter).filter(Boolean);
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 ? path10.dirname(configFile) : info?.config.rootDir ?? process.cwd();
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
- stopCodeReach
5992
+ startResourceLedger,
5993
+ stopCodeReach,
5994
+ stopResourceLedger
5082
5995
  });