@piwitests/reporter 0.44.0 → 0.46.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/index.js CHANGED
@@ -43,7 +43,9 @@ __export(index_exports, {
43
43
  module.exports = __toCommonJS(index_exports);
44
44
 
45
45
  // src/public/reporter.ts
46
- var path18 = __toESM(require("path"));
46
+ var fs21 = __toESM(require("fs"));
47
+ var os9 = __toESM(require("os"));
48
+ var path23 = __toESM(require("path"));
47
49
 
48
50
  // src/internal/config/desktop.ts
49
51
  var fs = __toESM(require("fs"));
@@ -82,9 +84,12 @@ var DEFAULTS = {
82
84
  // Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
83
85
  captureCodeReach: false,
84
86
  captureServerTraces: true,
87
+ captureResources: true,
88
+ leakCheck: "report",
85
89
  sampleAriaOnPass: true,
86
90
  uploadManifest: true,
87
91
  defaultCapture: true,
92
+ checkBaseUrl: false,
88
93
  streaming: true,
89
94
  streamingBatchSize: 5,
90
95
  streamingBatchDelay: 2e3,
@@ -122,9 +127,12 @@ var PIWI_ENV_KEYS = {
122
127
  captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
123
128
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
124
129
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
130
+ captureResources: "PIWI_CAPTURE_RESOURCES",
131
+ leakCheck: "PIWI_LEAK_CHECK",
125
132
  sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
126
133
  uploadManifest: "PIWI_UPLOAD_MANIFEST",
127
134
  defaultCapture: "PIWI_DEFAULT_CAPTURE",
135
+ checkBaseUrl: "PIWI_CHECK_BASE_URL",
128
136
  inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
129
137
  pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
130
138
  outputFile: "PIWI_OUTPUT_FILE",
@@ -145,6 +153,10 @@ var PIWI_SELECTION_ENV = {
145
153
  hash: "PIWI_SELECTION_HASH",
146
154
  count: "PIWI_SELECTION_COUNT"
147
155
  };
156
+ var PIWI_ORIGIN_ENV = {
157
+ kind: "PIWI_ORIGIN",
158
+ ref: "PIWI_ORIGIN_REF"
159
+ };
148
160
  var PIWI_SHARD_ENV = "PIWI_SHARD";
149
161
  var PIWI_PROBE_ENV = {
150
162
  flag: "PIWI_PROBE",
@@ -156,6 +168,7 @@ var PIWI_FLAKE_ENV = {
156
168
  plan: "PIWI_FLAKE_PLAN",
157
169
  results: "PIWI_FLAKE_RESULTS"
158
170
  };
171
+ var PIWI_RESOURCES_RESULTS_ENV = "PIWI_RESOURCES_RESULTS";
159
172
  function readBool(val) {
160
173
  if (val === void 0) return void 0;
161
174
  return val === "true";
@@ -184,9 +197,12 @@ var ENV_FALLBACK_SPECS = [
184
197
  { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
185
198
  { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
186
199
  { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
200
+ { option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
201
+ { option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
187
202
  { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
188
203
  { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
189
204
  { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
205
+ { option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
190
206
  { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
191
207
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
192
208
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
@@ -234,6 +250,7 @@ function applyOptionsToEnv(options) {
234
250
  if (options.label) env[PIWI_ENV_KEYS.label] = options.label;
235
251
  if (options.runLabel) env[PIWI_ENV_KEYS.runLabel] = options.runLabel;
236
252
  if (options.keep) env[PIWI_ENV_KEYS.keep] = "true";
253
+ if (options.checkBaseUrl !== void 0) env[PIWI_ENV_KEYS.checkBaseUrl] = String(options.checkBaseUrl);
237
254
  if (options.captureLocators === false || options.collectPerformanceMetrics === false)
238
255
  env[PIWI_ENV_KEYS.captureLocators] = "false";
239
256
  else if (options.captureLocators === true) env[PIWI_ENV_KEYS.captureLocators] = "true";
@@ -250,6 +267,8 @@ function applyOptionsToEnv(options) {
250
267
  if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
251
268
  env[PIWI_ENV_KEYS.captureServerTraces] = "false";
252
269
  else if (options.captureServerTraces === true) env[PIWI_ENV_KEYS.captureServerTraces] = "true";
270
+ if (options.captureResources !== void 0) env[PIWI_ENV_KEYS.captureResources] = String(options.captureResources);
271
+ if (options.leakCheck !== void 0) env[PIWI_ENV_KEYS.leakCheck] = options.leakCheck;
253
272
  if (options.sampleAriaOnPass === false) env[PIWI_ENV_KEYS.sampleAriaOnPass] = "false";
254
273
  else if (options.sampleAriaOnPass === true) env[PIWI_ENV_KEYS.sampleAriaOnPass] = "true";
255
274
  if (options.inspectOnFailure !== void 0) env[PIWI_ENV_KEYS.inspectOnFailure] = String(options.inspectOnFailure);
@@ -490,7 +509,7 @@ var HttpClient = class {
490
509
  }
491
510
  /** Unified request core: transport, headers, auth, response accumulation, timeout. */
492
511
  request(method, pathname, opts) {
493
- return new Promise((resolve6, reject) => {
512
+ return new Promise((resolve7, reject) => {
494
513
  if (this.closedReason !== null) {
495
514
  reject(new Error(this.closedReason));
496
515
  return;
@@ -513,7 +532,7 @@ var HttpClient = class {
513
532
  data += chunk;
514
533
  });
515
534
  res.on("end", () => {
516
- resolve6({ status: res.statusCode ?? 0, text: data, headers: res.headers });
535
+ resolve7({ status: res.statusCode ?? 0, text: data, headers: res.headers });
517
536
  });
518
537
  res.on("error", reject);
519
538
  res.on("close", () => {
@@ -608,6 +627,7 @@ function toWireTestCase(tc) {
608
627
  locatorSnapshots: rest.locatorSnapshots || null,
609
628
  locatorPages: rest.locatorPages || null,
610
629
  codeReach: rest.codeReach || null,
630
+ resources: rest.resources ?? null,
611
631
  didNotRunReason: rest.didNotRunReason ?? null,
612
632
  expectedStatus: rest.expectedStatus ?? null,
613
633
  blockedBy: rest.blockedBy ?? null
@@ -674,6 +694,7 @@ function serializeRun(payload, opts) {
674
694
  isFullRun: payload.isFullRun ?? true,
675
695
  filterDetails: payload.filterDetails ?? null
676
696
  };
697
+ if (payload.resourceReport) body.resourceReport = payload.resourceReport;
677
698
  if (opts.includeTestCases) {
678
699
  body.testCases = payload.testCases.map((tc) => toWireTestCase(tc));
679
700
  }
@@ -995,6 +1016,8 @@ var CrashRecovery = class {
995
1016
  if (!data) return;
996
1017
  this.logger.info("Found saved test data from a previous run, uploading...");
997
1018
  delete data.instanceId;
1019
+ const metadata = data.metadata && typeof data.metadata === "object" ? data.metadata : {};
1020
+ data.metadata = { ...metadata, ingestHealth: { submitFallback: { path: "recovery" } } };
998
1021
  try {
999
1022
  await httpClient.postJSON("/api/test-runs/submit", data, auth);
1000
1023
  this.logger.info("Successfully uploaded saved test data");
@@ -1056,7 +1079,8 @@ var ATTACHMENT_NAMES = {
1056
1079
  pageState: "piwi-page-state",
1057
1080
  userPick: "piwi-user-pick",
1058
1081
  aiUsage: "piwi-ai-usage",
1059
- aiMeta: "piwi-ai-meta"
1082
+ aiMeta: "piwi-ai-meta",
1083
+ resources: "piwi-resources"
1060
1084
  };
1061
1085
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
1062
1086
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
@@ -1277,8 +1301,8 @@ var FileHandler = class {
1277
1301
  }
1278
1302
  if (!lastPath) return null;
1279
1303
  const hash = crypto2.createHash("sha256");
1280
- await new Promise((resolve6, reject) => {
1281
- fs6.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve6).on("error", reject);
1304
+ await new Promise((resolve7, reject) => {
1305
+ fs6.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve7).on("error", reject);
1282
1306
  });
1283
1307
  return {
1284
1308
  tracePath: lastPath,
@@ -1308,6 +1332,35 @@ function formatMiB(bytes) {
1308
1332
 
1309
1333
  // src/internal/collect/metadata-collector.ts
1310
1334
  var import_node_child_process = require("child_process");
1335
+
1336
+ // ../core/src/wire.ts
1337
+ var RUN_ORIGIN_KINDS = [
1338
+ "ci",
1339
+ "ci-rerun",
1340
+ "local",
1341
+ "desktop",
1342
+ "editor",
1343
+ "preflight",
1344
+ "bug",
1345
+ "flake-lab",
1346
+ "probe",
1347
+ "bisect",
1348
+ "reproduce",
1349
+ "import"
1350
+ ];
1351
+ var RUN_ORIGIN_METADATA_KEY = "piwiOrigin";
1352
+ var RUN_ORIGIN_REF_MAX_LENGTH = 200;
1353
+ var RUN_ORIGIN_REF = /^[A-Za-z0-9._:/#@-]+$/;
1354
+ function isRunOriginKind(value) {
1355
+ return typeof value === "string" && RUN_ORIGIN_KINDS.includes(value);
1356
+ }
1357
+ function parseRunOriginRef(value) {
1358
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
1359
+ const ref = String(value).trim();
1360
+ return ref && ref.length <= RUN_ORIGIN_REF_MAX_LENGTH && RUN_ORIGIN_REF.test(ref) ? ref : void 0;
1361
+ }
1362
+
1363
+ // src/internal/collect/metadata-collector.ts
1311
1364
  var DETACHED_HEAD = "HEAD";
1312
1365
  function resolveScmBranch(env, gitBranch) {
1313
1366
  const pick = (...names) => {
@@ -1352,6 +1405,19 @@ function resolveScmBaseBranch(env) {
1352
1405
  const trimmed = target?.trim();
1353
1406
  return trimmed ? normalizeRef(trimmed) : void 0;
1354
1407
  }
1408
+ function detectCi(env) {
1409
+ return Boolean(
1410
+ env.JENKINS_URL || env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.TRAVIS || env.TF_BUILD || env.BITBUCKET_BUILD_NUMBER || env.CI && env.CI !== "false" && env.CI !== "0"
1411
+ );
1412
+ }
1413
+ function resolveRunOrigin(env, warn) {
1414
+ const raw = env[PIWI_ORIGIN_ENV.kind]?.trim();
1415
+ let kind = detectCi(env) ? "ci" : "local";
1416
+ if (raw && isRunOriginKind(raw)) kind = raw;
1417
+ else if (raw) warn?.(`Ignoring ${PIWI_ORIGIN_ENV.kind}=${raw}: not a known run origin, recording "${kind}"`);
1418
+ const ref = parseRunOriginRef(env[PIWI_ORIGIN_ENV.ref]);
1419
+ return ref === void 0 ? { kind } : { kind, ref };
1420
+ }
1355
1421
  function normalizeRef(ref) {
1356
1422
  return ref.replace(/^refs\/heads\//, "");
1357
1423
  }
@@ -1361,7 +1427,10 @@ var MetadataCollector = class {
1361
1427
  }
1362
1428
  /** Collect all available metadata from the environment, config, and suite */
1363
1429
  collect(config, suite, options) {
1364
- const metadata = { workingDir: process.cwd() };
1430
+ const metadata = {
1431
+ workingDir: process.cwd(),
1432
+ [RUN_ORIGIN_METADATA_KEY]: resolveRunOrigin(process.env, (message) => this.logger.warn(message))
1433
+ };
1365
1434
  if (options.projectDescription) metadata.projectDescription = options.projectDescription;
1366
1435
  if (options.relatedIssue) metadata.relatedIssue = options.relatedIssue;
1367
1436
  if (options.ciInfo) metadata.ciInfo = options.ciInfo;
@@ -1538,6 +1607,15 @@ var MetadataCollector = class {
1538
1607
  ci.buildUrl = `${env.SYSTEM_TEAMFOUNDATIONSERVERURI}${env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${env.BUILD_BUILDID}`;
1539
1608
  }
1540
1609
  ci.jobName = env.AGENT_JOBNAME;
1610
+ } else if (env.BITBUCKET_BUILD_NUMBER) {
1611
+ ci.provider = "Bitbucket Pipelines";
1612
+ ci.buildNumber = env.BITBUCKET_BUILD_NUMBER;
1613
+ ci.pipelineId = env.BITBUCKET_PIPELINE_UUID;
1614
+ ci.jobId = env.BITBUCKET_STEP_UUID;
1615
+ ci.repository = env.BITBUCKET_REPO_FULL_NAME;
1616
+ if (env.BITBUCKET_REPO_FULL_NAME) {
1617
+ ci.buildUrl = `https://bitbucket.org/${env.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${env.BITBUCKET_BUILD_NUMBER}`;
1618
+ }
1541
1619
  } else if (env.CI) {
1542
1620
  ci.provider = "Unknown CI";
1543
1621
  ci.detected = true;
@@ -1709,10 +1787,10 @@ function createLimiter(maxConcurrent) {
1709
1787
  if (run) run();
1710
1788
  };
1711
1789
  return function limit(fn) {
1712
- return new Promise((resolve6, reject) => {
1790
+ return new Promise((resolve7, reject) => {
1713
1791
  const run = () => {
1714
1792
  active++;
1715
- Promise.resolve().then(fn).then(resolve6, reject).finally(next);
1793
+ Promise.resolve().then(fn).then(resolve7, reject).finally(next);
1716
1794
  };
1717
1795
  if (active < limitValue) run();
1718
1796
  else queue.push(run);
@@ -2151,7 +2229,7 @@ var StreamManager = class {
2151
2229
  this.logger.debugError(
2152
2230
  `${this.pendingEvents.length} events pending, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`
2153
2231
  );
2154
- await new Promise((resolve6) => setTimeout(resolve6, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
2232
+ await new Promise((resolve7) => setTimeout(resolve7, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
2155
2233
  }
2156
2234
  if (!this.pendingEvents.isEmpty) {
2157
2235
  const remaining = this.pendingEvents.takeAll();
@@ -2208,7 +2286,7 @@ var StreamManager = class {
2208
2286
  if (flush) await flush;
2209
2287
  const delays = [0, 1e3, 3e3];
2210
2288
  for (let attempt = 0; attempt < delays.length; attempt++) {
2211
- if (delays[attempt]) await new Promise((resolve6) => setTimeout(resolve6, delays[attempt]));
2289
+ if (delays[attempt]) await new Promise((resolve7) => setTimeout(resolve7, delays[attempt]));
2212
2290
  try {
2213
2291
  await this.limitLiveUpload(
2214
2292
  () => this.uploader.uploadCaseFiles(
@@ -3686,6 +3764,131 @@ function isDueForAriaSample(testInfo) {
3686
3764
  return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
3687
3765
  }
3688
3766
 
3767
+ // src/internal/support/base-url-check.ts
3768
+ var BASE_URL_CHECK_ATTEMPTS = 3;
3769
+ var BASE_URL_CHECK_TIMEOUT_MS = 1e4;
3770
+ var BASE_URL_CHECK_DELAY_MS = 2e3;
3771
+ var GATEWAY_ANSWERS = {
3772
+ 502: "bad gateway",
3773
+ 503: "service unavailable",
3774
+ 504: "gateway timeout"
3775
+ };
3776
+ function cliProjectNames(argv = process.argv) {
3777
+ let names = null;
3778
+ for (let i = 0; i < argv.length; i++) {
3779
+ const arg = argv[i];
3780
+ if (arg.startsWith("--project=")) {
3781
+ (names ??= []).push(arg.slice("--project=".length));
3782
+ } else if (arg === "--project") {
3783
+ names ??= [];
3784
+ while (i + 1 < argv.length && !argv[i + 1].startsWith("-")) names.push(argv[++i]);
3785
+ }
3786
+ }
3787
+ return names;
3788
+ }
3789
+ function wildcard(pattern) {
3790
+ const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
3791
+ return new RegExp(`^${escaped.join(".*")}$`, "i");
3792
+ }
3793
+ function selectedProjects(projects, argv = process.argv) {
3794
+ const names = cliProjectNames(argv);
3795
+ if (!names || names.length === 0) return projects;
3796
+ const patterns = names.map(wildcard);
3797
+ const chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
3798
+ if (!argv.includes("--no-deps")) {
3799
+ const byName = new Map(projects.map((p) => [p.name ?? "", p]));
3800
+ const queue = [...chosen];
3801
+ while (queue.length > 0) {
3802
+ for (const dep of queue.shift().dependencies ?? []) {
3803
+ const project = byName.get(dep);
3804
+ if (project && !chosen.has(project)) {
3805
+ chosen.add(project);
3806
+ queue.push(project);
3807
+ }
3808
+ }
3809
+ }
3810
+ }
3811
+ return projects.filter((p) => chosen.has(p));
3812
+ }
3813
+ function asProxy(value) {
3814
+ const proxy = value;
3815
+ return proxy && typeof proxy === "object" && typeof proxy.server === "string" ? proxy : void 0;
3816
+ }
3817
+ function baseUrlTargets(config, argv = process.argv) {
3818
+ const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
3819
+ const targets = /* @__PURE__ */ new Map();
3820
+ for (const project of selectedProjects(projects, argv)) {
3821
+ const url = project.use?.baseURL;
3822
+ if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
3823
+ const target = targets.get(url);
3824
+ if (target) {
3825
+ if (project.name) target.projects.push(project.name);
3826
+ continue;
3827
+ }
3828
+ targets.set(url, {
3829
+ url,
3830
+ projects: project.name ? [project.name] : [],
3831
+ ignoreHTTPSErrors: project.use?.ignoreHTTPSErrors === true,
3832
+ proxy: asProxy(project.use?.proxy)
3833
+ });
3834
+ }
3835
+ return [...targets.values()];
3836
+ }
3837
+ function requestError(error) {
3838
+ const first = errorMessage(error).split("\n")[0].trim();
3839
+ return first.replace(/^apiRequestContext\.\w+:\s*/, "");
3840
+ }
3841
+ var playwrightProbe = async (target, timeoutMs) => {
3842
+ const { request } = await import("@playwright/test");
3843
+ const context = await request.newContext({ ignoreHTTPSErrors: target.ignoreHTTPSErrors, proxy: target.proxy });
3844
+ try {
3845
+ const response = await context.get(target.url, { timeout: timeoutMs, maxRedirects: 0, failOnStatusCode: false });
3846
+ return { status: response.status() };
3847
+ } catch (error) {
3848
+ return { error: requestError(error) };
3849
+ } finally {
3850
+ await context.dispose().catch(() => {
3851
+ });
3852
+ }
3853
+ };
3854
+ async function checkBaseUrl(target, options = {}) {
3855
+ const attempts = Math.max(1, options.attempts ?? BASE_URL_CHECK_ATTEMPTS);
3856
+ const timeoutMs = options.timeoutMs ?? BASE_URL_CHECK_TIMEOUT_MS;
3857
+ const delayMs = options.delayMs ?? BASE_URL_CHECK_DELAY_MS;
3858
+ const probe = options.probe ?? playwrightProbe;
3859
+ let reason = "";
3860
+ for (let attempt = 1; attempt <= attempts; attempt++) {
3861
+ const outcome = await probe(target, timeoutMs);
3862
+ if ("status" in outcome) {
3863
+ const gateway = GATEWAY_ANSWERS[outcome.status];
3864
+ if (!gateway) return null;
3865
+ reason = `${outcome.status} ${gateway}`;
3866
+ } else {
3867
+ reason = outcome.error;
3868
+ }
3869
+ if (attempt < attempts) await new Promise((resolve7) => setTimeout(resolve7, delayMs));
3870
+ }
3871
+ return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
3872
+ }
3873
+ async function checkBaseUrls(config, options = {}) {
3874
+ const targets = baseUrlTargets(config, options.argv);
3875
+ const reasons = await Promise.all(targets.map((target) => checkBaseUrl(target, options)));
3876
+ const down = targets.flatMap((target, i) => reasons[i] ? [{ target, reason: reasons[i] }] : []);
3877
+ if (down.length === 0) return targets;
3878
+ const lines = down.map(({ target, reason }) => {
3879
+ const used = target.projects.length > 0 ? ` (${target.projects.join(", ")})` : "";
3880
+ return ` ${target.url}${used}: ${reason}`;
3881
+ });
3882
+ const message = [
3883
+ `[Piwi Dashboard] The app under test did not answer, so no test ran:`,
3884
+ ...lines,
3885
+ `Bring the environment back and run again, or set checkBaseUrl: false (PIWI_CHECK_BASE_URL=false) to run the tests anyway.`
3886
+ ].join("\n");
3887
+ const error = new Error(message);
3888
+ error.stack = message;
3889
+ throw error;
3890
+ }
3891
+
3689
3892
  // src/internal/manifest/collect.ts
3690
3893
  var fs12 = __toESM(require("fs"));
3691
3894
  var path11 = __toESM(require("path"));
@@ -3785,6 +3988,10 @@ function createGlobalSetup(options, userSetup) {
3785
3988
  if (userSetup) return userSetup(config);
3786
3989
  return;
3787
3990
  }
3991
+ if (opts.checkBaseUrl === true) {
3992
+ const checked = await checkBaseUrls(config ?? {});
3993
+ for (const target of checked) logger.debug(`Base URL answered: ${target.url}`);
3994
+ }
3788
3995
  if (opts.enabled === false || !opts.serverUrl) {
3789
3996
  logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
3790
3997
  if (userSetup) return userSetup(config);
@@ -3892,7 +4099,7 @@ var path13 = __toESM(require("path"));
3892
4099
 
3893
4100
  // src/internal/probe/plan.ts
3894
4101
  function probeRunMetadata(base = {}) {
3895
- return { ...base, piwiProbe: true };
4102
+ return { ...base, piwiProbe: true, [RUN_ORIGIN_METADATA_KEY]: { kind: "probe" } };
3896
4103
  }
3897
4104
  function parseRouteKey(routeKey2) {
3898
4105
  const space = routeKey2.indexOf(" ");
@@ -3900,8 +4107,8 @@ function parseRouteKey(routeKey2) {
3900
4107
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
3901
4108
  }
3902
4109
  function patternToRegex(pattern) {
3903
- const path27 = pattern.split("?")[0] ?? pattern;
3904
- const escaped = path27.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
4110
+ const path31 = pattern.split("?")[0] ?? pattern;
4111
+ const escaped = path31.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
3905
4112
  return new RegExp(`^${escaped}$`);
3906
4113
  }
3907
4114
  function suitePathEqual(a, b) {
@@ -3924,13 +4131,13 @@ function matchProbeItem(plan, test) {
3924
4131
  function requestMatchesRoute(method, url, routeKey2) {
3925
4132
  const { method: rm, pattern } = parseRouteKey(routeKey2);
3926
4133
  if (rm && rm !== method.toUpperCase()) return false;
3927
- let path27;
4134
+ let path31;
3928
4135
  try {
3929
- path27 = new URL(url).pathname;
4136
+ path31 = new URL(url).pathname;
3930
4137
  } catch {
3931
- path27 = url.split("?")[0] ?? url;
4138
+ path31 = url.split("?")[0] ?? url;
3932
4139
  }
3933
- return patternToRegex(pattern).test(path27);
4140
+ return patternToRegex(pattern).test(path31);
3934
4141
  }
3935
4142
  function createProbeState() {
3936
4143
  return { navigated: false, matches: 0 };
@@ -3952,10 +4159,10 @@ function isProbeMode() {
3952
4159
  function loadPlan() {
3953
4160
  if (planCache !== void 0) return planCache;
3954
4161
  planCache = null;
3955
- const path27 = process.env[PIWI_PROBE_ENV.plan];
3956
- if (path27) {
4162
+ const path31 = process.env[PIWI_PROBE_ENV.plan];
4163
+ if (path31) {
3957
4164
  try {
3958
- planCache = JSON.parse(fs14.readFileSync(path27, "utf8"));
4165
+ planCache = JSON.parse(fs14.readFileSync(path31, "utf8"));
3959
4166
  } catch {
3960
4167
  planCache = null;
3961
4168
  }
@@ -3977,10 +4184,10 @@ function outcomeFromStatus(status, applied) {
3977
4184
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
3978
4185
  }
3979
4186
  function recordProbeOutcome(line) {
3980
- const path27 = process.env[PIWI_PROBE_ENV.results];
3981
- if (!path27) return;
4187
+ const path31 = process.env[PIWI_PROBE_ENV.results];
4188
+ if (!path31) return;
3982
4189
  try {
3983
- fs14.appendFileSync(path27, `${JSON.stringify(line)}
4190
+ fs14.appendFileSync(path31, `${JSON.stringify(line)}
3984
4191
  `);
3985
4192
  } catch {
3986
4193
  }
@@ -4364,7 +4571,7 @@ function originOf(url) {
4364
4571
  return null;
4365
4572
  }
4366
4573
  }
4367
- var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, Math.max(0, ms)));
4574
+ var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, Math.max(0, ms)));
4368
4575
  async function performRouteAction(route, action, startedAt) {
4369
4576
  switch (action.kind) {
4370
4577
  case "abort":
@@ -4501,7 +4708,12 @@ function loadFlakePlan() {
4501
4708
  }
4502
4709
  }
4503
4710
  function flakeRunMetadata(stamp, base = {}) {
4504
- return { ...base, [FLAKE_LAB_RUN_METADATA_KEY]: { experimentId: stamp.experimentId, armId: stamp.armId } };
4711
+ const ref = parseRunOriginRef(stamp.experimentId);
4712
+ return {
4713
+ ...base,
4714
+ [FLAKE_LAB_RUN_METADATA_KEY]: { experimentId: stamp.experimentId, armId: stamp.armId },
4715
+ [RUN_ORIGIN_METADATA_KEY]: ref === void 0 ? { kind: "flake-lab" } : { kind: "flake-lab", ref }
4716
+ };
4505
4717
  }
4506
4718
  function relativeTestFile2(file) {
4507
4719
  return file ? path14.relative(process.cwd(), file).split(path14.sep).join("/") : null;
@@ -5061,6 +5273,15 @@ function emitGitLabDotenv(output, env, logger) {
5061
5273
  logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
5062
5274
  }
5063
5275
  }
5276
+ function emitResourceSummary(lines, logger, env = process.env) {
5277
+ if (!env.GITHUB_ACTIONS || !env.GITHUB_STEP_SUMMARY || lines.length === 0) return;
5278
+ appendFileLines(
5279
+ env.GITHUB_STEP_SUMMARY,
5280
+ ["### Piwi resources", "", "```text", ...lines, "```", ""],
5281
+ logger,
5282
+ "step summary"
5283
+ );
5284
+ }
5064
5285
  function appendFileLines(file, lines, logger, label) {
5065
5286
  try {
5066
5287
  fs17.appendFileSync(file, lines.join("\n") + "\n");
@@ -5076,6 +5297,9 @@ function ciBuildUrlFromMetadata(metadata) {
5076
5297
 
5077
5298
  // src/internal/submit/run-submitter.ts
5078
5299
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
5300
+ function markSubmitFallback(run, path31, reason) {
5301
+ run.metadata.ingestHealth = { submitFallback: { path: path31, reason } };
5302
+ }
5079
5303
  var RunSubmitter = class {
5080
5304
  /**
5081
5305
  * @param httpClient HTTP client for auth resolution and the finish call.
@@ -5151,13 +5375,21 @@ var RunSubmitter = class {
5151
5375
  }
5152
5376
  if (!sm) await this.recovery.tryUpload(this.httpClient, auth);
5153
5377
  let outcome = { done: false, output: null };
5154
- if (sm?.enabled && sm?.runId != null && !sm.lostResults) {
5155
- outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
5378
+ let fallbackReason = null;
5379
+ if (sm?.enabled && sm?.runId != null) {
5380
+ if (sm.lostResults) fallbackReason = "results-lost";
5381
+ else {
5382
+ outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
5383
+ if (!outcome.done) fallbackReason = "finish-failed";
5384
+ }
5156
5385
  }
5157
5386
  if (!outcome.done && !this.httpClient.closed && (this.hasReports(run) || run.options.uploadTraces)) {
5387
+ if (fallbackReason) markSubmitFallback(run, "upload", fallbackReason);
5158
5388
  outcome = await this.tryUploadWithFiles(run, overallStatus, duration, auth);
5389
+ if (!outcome.done) fallbackReason = "upload-failed";
5159
5390
  }
5160
5391
  if (!outcome.done) {
5392
+ if (fallbackReason) markSubmitFallback(run, "submit", fallbackReason);
5161
5393
  outcome = await this.tryUploadJSON(run, overallStatus, duration, auth);
5162
5394
  }
5163
5395
  if (outcome.output) {
@@ -5212,7 +5444,8 @@ var RunSubmitter = class {
5212
5444
  shardIndex: run.shardInfo?.current,
5213
5445
  shardTotal: run.shardInfo?.total,
5214
5446
  isFullRun: run.isFullRun,
5215
- filterDetails: run.filterDetails
5447
+ filterDetails: run.filterDetails,
5448
+ resourceReport: run.resourceReport ?? null
5216
5449
  };
5217
5450
  }
5218
5451
  async tryFinishStreaming(run, overallStatus, duration, auth) {
@@ -5243,6 +5476,7 @@ var RunSubmitter = class {
5243
5476
  isFullRun: run.isFullRun,
5244
5477
  filterDetails: run.filterDetails ?? null
5245
5478
  };
5479
+ if (run.resourceReport) finishBody.resourceReport = run.resourceReport;
5246
5480
  if (run.shardInfo) {
5247
5481
  finishBody.shardIndex = run.shardInfo.current;
5248
5482
  finishBody.shardTotal = run.shardInfo.total;
@@ -5327,165 +5561,2499 @@ var RunSubmitter = class {
5327
5561
  }
5328
5562
  };
5329
5563
 
5330
- // ../core/src/describe-failure.ts
5331
- var HEADLINE_MAX_CHARS = 120;
5332
- var VALUE_MAX_CHARS = 40;
5333
- var SHORT_VALUE_MAX_CHARS = 20;
5334
- var MASK_TOKEN_RE = /<(?:N|VALUE|URL|STR|UUID|HASH|EMAIL)>/g;
5335
- var ACTION_VERBS = {
5336
- click: "click",
5337
- dblclick: "double-click",
5338
- fill: "fill",
5339
- type: "type",
5340
- press: "press",
5341
- pressSequentially: "type",
5342
- check: "check",
5343
- uncheck: "uncheck",
5344
- hover: "hover",
5345
- tap: "tap",
5346
- focus: "focus",
5347
- blur: "blur",
5348
- clear: "clear",
5349
- selectOption: "select",
5350
- selectText: "select text",
5351
- setInputFiles: "file upload",
5352
- setChecked: "check",
5353
- dragTo: "drag",
5354
- dragAndDrop: "drag",
5355
- scrollIntoViewIfNeeded: "scroll",
5356
- screenshot: "screenshot",
5357
- waitFor: "wait",
5358
- waitForSelector: "waitForSelector",
5359
- waitForLoadState: "waitForLoadState",
5360
- waitForFunction: "waitForFunction",
5361
- waitForResponse: "waitForResponse",
5362
- waitForRequest: "waitForRequest",
5363
- waitForEvent: "waitForEvent",
5364
- waitForTimeout: "waitForTimeout",
5365
- innerText: "read text",
5366
- textContent: "read text",
5367
- inputValue: "read value",
5368
- getAttribute: "read attribute",
5369
- isVisible: "visibility check",
5370
- isEnabled: "enabled check",
5371
- isChecked: "checked check",
5372
- boundingBox: "measure",
5373
- evaluate: "evaluate",
5374
- goto: "navigation"
5375
- };
5376
- var ACTION_GERUNDS = {
5377
- click: "clicking",
5378
- dblclick: "double-clicking",
5379
- fill: "filling",
5380
- type: "typing into",
5381
- press: "pressing a key on",
5382
- pressSequentially: "typing into",
5383
- check: "checking",
5384
- uncheck: "unchecking",
5385
- hover: "hovering",
5386
- tap: "tapping",
5387
- focus: "focusing",
5388
- clear: "clearing",
5389
- selectOption: "selecting an option in",
5390
- setInputFiles: "uploading a file to",
5391
- dragTo: "dragging",
5392
- waitFor: "waiting for",
5393
- waitForSelector: "waiting for",
5394
- evaluate: "evaluating on"
5395
- };
5396
- var STATE_PHRASES = {
5397
- "not-found": "was not found on the page",
5398
- hidden: "never became visible",
5399
- "not-visible": "never became visible",
5400
- "not-enabled": "never became enabled",
5401
- "not-editable": "never became editable",
5402
- "not-stable": "never stopped moving",
5403
- "outside-viewport": "stayed outside the viewport",
5404
- "intercepts-pointer": "was covered by another element",
5405
- detached: "was detached from the DOM",
5406
- navigating: "was still navigating"
5407
- };
5408
- var STATE_MATCHERS = {
5409
- toBeVisible: { met: "visible", unmet: "never became visible" },
5410
- toBeHidden: { met: "hidden", unmet: "never became hidden" },
5411
- toBeEnabled: { met: "enabled", unmet: "never became enabled" },
5412
- toBeDisabled: { met: "disabled", unmet: "never became disabled" },
5413
- toBeChecked: { met: "checked", unmet: "never became checked" },
5414
- toBeEditable: { met: "editable", unmet: "never became editable" },
5415
- toBeFocused: { met: "focused", unmet: "never received focus" },
5416
- toBeAttached: { met: "attached", unmet: "never appeared in the DOM" },
5417
- toBeDetached: { met: "detached", unmet: "never left the DOM" },
5418
- toBeInViewport: { met: "in the viewport", unmet: "never entered the viewport" },
5419
- toBeEmpty: { met: "empty", unmet: "never became empty" }
5420
- };
5421
- var VALUE_MATCHERS = {
5422
- toHaveText: "text",
5423
- toContainText: "text containing",
5424
- toHaveValue: "value",
5425
- toHaveValues: "values",
5426
- toHaveAttribute: "attribute",
5427
- toHaveClass: "class",
5428
- toContainClass: "class",
5429
- toHaveId: "id",
5430
- toHaveCSS: "CSS",
5431
- toHaveJSProperty: "property",
5432
- toHaveAccessibleName: "accessible name",
5433
- toHaveAccessibleDescription: "accessible description",
5434
- toHaveRole: "role",
5435
- toHaveURL: "URL",
5436
- toHaveTitle: "title",
5437
- toHaveScreenshot: "screenshot",
5438
- toMatchAriaSnapshot: "ARIA snapshot"
5439
- };
5440
- var NETWORK_ERRORS = {
5441
- ERR_CONNECTION_REFUSED: "Connection refused loading",
5442
- ERR_CONNECTION_RESET: "Connection reset loading",
5443
- ERR_CONNECTION_CLOSED: "Connection closed loading",
5444
- ERR_CONNECTION_TIMED_OUT: "Connection timed out loading",
5445
- ERR_TIMED_OUT: "Connection timed out loading",
5446
- ERR_NAME_NOT_RESOLVED: "DNS lookup failed for",
5447
- ERR_INTERNET_DISCONNECTED: "No network while loading",
5448
- ERR_ADDRESS_UNREACHABLE: "Address unreachable loading",
5449
- ERR_ABORTED: "Navigation aborted loading",
5450
- ERR_EMPTY_RESPONSE: "Empty response loading",
5451
- ERR_TOO_MANY_REDIRECTS: "Too many redirects loading",
5452
- ERR_SSL_PROTOCOL_ERROR: "TLS error loading",
5453
- ERR_CERT_AUTHORITY_INVALID: "Untrusted certificate loading",
5454
- ERR_CERT_COMMON_NAME_INVALID: "Certificate name mismatch loading",
5455
- ERR_CERT_DATE_INVALID: "Expired certificate loading",
5456
- ERR_BLOCKED_BY_CLIENT: "Request blocked loading",
5457
- ERR_FAILED: "Request failed loading",
5458
- ERR_HTTP_RESPONSE_CODE_FAILURE: "HTTP error loading",
5459
- NS_ERROR_CONNECTION_REFUSED: "Connection refused loading",
5460
- NS_ERROR_UNKNOWN_HOST: "DNS lookup failed for",
5461
- NS_ERROR_NET_TIMEOUT: "Connection timed out loading",
5462
- NS_ERROR_OFFLINE: "No network while loading",
5463
- NS_ERROR_ABORT: "Navigation aborted loading",
5464
- NS_BINDING_ABORTED: "Navigation aborted loading"
5465
- };
5466
- function formatTimeout(ms) {
5467
- if (ms < 1e3) return `${ms} ms`;
5468
- const seconds = ms / 1e3;
5469
- const rounded = Math.round(seconds * 10) / 10;
5470
- return `${Number.isInteger(rounded) ? rounded.toFixed(0) : rounded} s`;
5564
+ // src/internal/capture/resource-ledger.ts
5565
+ var fs19 = __toESM(require("fs"));
5566
+ var path20 = __toESM(require("path"));
5567
+ var import_node_module2 = require("module");
5568
+
5569
+ // src/internal/capture/quiet-capture.ts
5570
+ var import_node_module = require("module");
5571
+ var path18 = __toESM(require("path"));
5572
+ var BOXED_PREFIX_EXPORTS = [
5573
+ [
5574
+ "playwright-core/lib/coreBundle",
5575
+ (mod) => mod?.utils?.setBoxedStackPrefixes
5576
+ ],
5577
+ ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
5578
+ ];
5579
+ var boxing;
5580
+ var resolvedPlaywrightPackageJson;
5581
+ function resolvePlaywrightPackageJson() {
5582
+ if (resolvedPlaywrightPackageJson !== void 0) return resolvedPlaywrightPackageJson;
5583
+ try {
5584
+ const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
5585
+ resolvedPlaywrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
5586
+ } catch {
5587
+ resolvedPlaywrightPackageJson = null;
5588
+ }
5589
+ return resolvedPlaywrightPackageJson;
5471
5590
  }
5472
- function routeOf(url) {
5591
+ function resolveBoxing() {
5473
5592
  try {
5474
- const parsed = new URL(url);
5475
- return `${parsed.pathname}${parsed.search}` || "/";
5593
+ const root = findOwnPackageJson(__dirname)?.root;
5594
+ if (!root) return null;
5595
+ const playwrightPackageJson = resolvePlaywrightPackageJson();
5596
+ if (!playwrightPackageJson) return null;
5597
+ const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
5598
+ for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
5599
+ let set;
5600
+ try {
5601
+ set = pick(requireFromPlaywright(moduleId));
5602
+ } catch {
5603
+ continue;
5604
+ }
5605
+ if (typeof set !== "function") continue;
5606
+ return {
5607
+ set,
5608
+ prefixes: [path18.dirname(playwrightPackageJson), path18.join(root, "dist") + path18.sep]
5609
+ };
5610
+ }
5476
5611
  } catch {
5477
- return url;
5478
5612
  }
5613
+ return null;
5479
5614
  }
5480
- function truncateValue(value, max) {
5481
- const flat = value.replace(/\s+/g, " ").trim();
5482
- return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
5615
+ function boxCaptureFrames(extraPrefixes = []) {
5616
+ const first = boxing === void 0;
5617
+ if (first) boxing = resolveBoxing();
5618
+ if (!boxing) return;
5619
+ const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
5620
+ if (!first && added.length === 0) return;
5621
+ boxing.prefixes.push(...added);
5622
+ try {
5623
+ boxing.set([...boxing.prefixes]);
5624
+ } catch {
5625
+ }
5483
5626
  }
5484
- function countNoun(locator, count) {
5485
- const role = locator ? /getByRole\(\s*['"]([a-z]+)['"]/.exec(locator)?.[1] : null;
5486
- const noun = role ?? "element";
5487
- if (count === 1) return noun;
5488
- return noun.endsWith("s") ? noun : noun.endsWith("x") || noun.endsWith("ch") ? `${noun}es` : `${noun}s`;
5627
+ function internalCall(owner, run) {
5628
+ const wrap = owner?._wrapApiCall;
5629
+ if (typeof wrap !== "function") return run();
5630
+ return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
5631
+ }
5632
+
5633
+ // src/internal/capture/worker-metrics.ts
5634
+ var import_node_perf_hooks = require("perf_hooks");
5635
+
5636
+ // src/internal/support/system-readers.ts
5637
+ var fs18 = __toESM(require("fs"));
5638
+ var path19 = __toESM(require("path"));
5639
+ var CLK_TCK = 100;
5640
+ function readText(file) {
5641
+ try {
5642
+ return fs18.readFileSync(file, "utf8");
5643
+ } catch {
5644
+ return null;
5645
+ }
5646
+ }
5647
+ function parseProcStat(pid, text) {
5648
+ const open = text.indexOf("(");
5649
+ const close = text.lastIndexOf(")");
5650
+ if (open < 0 || close < open) return null;
5651
+ const fields = text.slice(close + 2).trim().split(/\s+/);
5652
+ const ppid = Number(fields[1]);
5653
+ const utime = Number(fields[11]);
5654
+ const stime = Number(fields[12]);
5655
+ const start = Number(fields[19]);
5656
+ if (![ppid, utime, stime, start].every(Number.isFinite)) return null;
5657
+ return { pid, ppid, comm: text.slice(open + 1, close), cpuMs: (utime + stime) * 1e3 / CLK_TCK, start };
5658
+ }
5659
+ function parseSchedstat(text) {
5660
+ const waitNs = Number(text.trim().split(/\s+/)[1]);
5661
+ return Number.isFinite(waitNs) ? waitNs / 1e6 : null;
5662
+ }
5663
+ function parseKbField(text, key) {
5664
+ const match = new RegExp(`^${key}:\\s+(\\d+)`, "m").exec(text);
5665
+ return match ? Number(match[1]) : null;
5666
+ }
5667
+ function parseCpuTicks(text) {
5668
+ const line = text.split("\n", 1)[0] ?? "";
5669
+ if (!line.startsWith("cpu ")) return null;
5670
+ const v = line.trim().split(/\s+/).slice(1, 9).map(Number);
5671
+ if (v.length < 8 || v.some((n) => !Number.isFinite(n))) return null;
5672
+ const [user, nice, system, idle, iowait, irq, softirq, steal] = v;
5673
+ return { busy: user + nice + system + irq + softirq, idle, iowait, steal, total: v.reduce((sum, n) => sum + n, 0) };
5674
+ }
5675
+ function parsePsiTotal(text) {
5676
+ const match = /^some\b.*\btotal=(\d+)/m.exec(text);
5677
+ return match ? Number(match[1]) : null;
5678
+ }
5679
+ function parsePsTime(text) {
5680
+ const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/.exec(text.trim());
5681
+ if (!match) return null;
5682
+ const [, days, hours, minutes, seconds] = match;
5683
+ return ((Number(days ?? 0) * 24 + Number(hours ?? 0)) * 3600 + Number(minutes) * 60 + Number(seconds)) * 1e3;
5684
+ }
5685
+ function parsePsOutput(text) {
5686
+ const out = [];
5687
+ for (const line of text.split("\n")) {
5688
+ const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
5689
+ if (!match) continue;
5690
+ const cpuMs = parsePsTime(match[4]);
5691
+ if (cpuMs === null) continue;
5692
+ out.push({
5693
+ pid: Number(match[1]),
5694
+ ppid: Number(match[2]),
5695
+ rssKb: Number(match[3]),
5696
+ cpuMs,
5697
+ command: match[5].trim()
5698
+ });
5699
+ }
5700
+ return out;
5701
+ }
5702
+ function parseWindowsProcesses(text) {
5703
+ let data;
5704
+ try {
5705
+ data = JSON.parse(text.replace(/^\uFEFF/, ""));
5706
+ } catch {
5707
+ return [];
5708
+ }
5709
+ const rows = Array.isArray(data) ? data : [data];
5710
+ const out = [];
5711
+ for (const row of rows) {
5712
+ if (typeof row !== "object" || row === null) continue;
5713
+ const record = row;
5714
+ const pid = Number(record.ProcessId);
5715
+ const ppid = Number(record.ParentProcessId);
5716
+ if (!Number.isFinite(pid) || !Number.isFinite(ppid)) continue;
5717
+ const workingSet = Number(record.WorkingSetSize);
5718
+ const user = Number(record.UserModeTime);
5719
+ const kernel = Number(record.KernelModeTime);
5720
+ out.push({
5721
+ pid,
5722
+ ppid,
5723
+ rssKb: Number.isFinite(workingSet) ? Math.round(workingSet / 1024) : 0,
5724
+ cpuMs: ((Number.isFinite(user) ? user : 0) + (Number.isFinite(kernel) ? kernel : 0)) / 1e4,
5725
+ name: typeof record.Name === "string" ? record.Name : "",
5726
+ commandLine: typeof record.CommandLine === "string" ? record.CommandLine : ""
5727
+ });
5728
+ }
5729
+ return out;
5730
+ }
5731
+ function roleOf(comm, cmdline) {
5732
+ const command = `${comm} ${cmdline}`;
5733
+ if (/workerProcessEntry/.test(cmdline)) return "worker";
5734
+ if (/loaderProcessEntry/.test(cmdline)) return "runner";
5735
+ if (/(^|[\s/])ffmpeg/.test(command)) return "ffmpeg";
5736
+ const type = /--type=([\w-]+)/.exec(cmdline)?.[1];
5737
+ if (/chrom|headless_shell|msedge|Google Chrome/i.test(command)) {
5738
+ if (!type) return "browser";
5739
+ if (type === "renderer") return "renderer";
5740
+ if (type === "gpu-process") return "gpu";
5741
+ return "utility";
5742
+ }
5743
+ if (/firefox/i.test(command)) return /-contentproc/.test(cmdline) ? "renderer" : "browser";
5744
+ if (/WebKitWebProcess/.test(command)) return "renderer";
5745
+ if (/WebKitGPUProcess/.test(command)) return "gpu";
5746
+ if (/WebKitNetworkProcess/.test(command)) return "utility";
5747
+ if (/MiniBrowser|pw_run\.sh/.test(command)) return "browser";
5748
+ return null;
5749
+ }
5750
+ function descendantsOf(root, table) {
5751
+ const children = /* @__PURE__ */ new Map();
5752
+ for (const proc of table) {
5753
+ const list = children.get(proc.ppid) ?? [];
5754
+ list.push(proc);
5755
+ children.set(proc.ppid, list);
5756
+ }
5757
+ const out = [];
5758
+ const stack = [root];
5759
+ const seen = /* @__PURE__ */ new Set([root]);
5760
+ while (stack.length > 0) {
5761
+ for (const child of children.get(stack.pop()) ?? []) {
5762
+ if (seen.has(child.pid)) continue;
5763
+ seen.add(child.pid);
5764
+ out.push(child);
5765
+ stack.push(child.pid);
5766
+ }
5767
+ }
5768
+ return out;
5769
+ }
5770
+ var ProcFs = class {
5771
+ constructor(root = "/proc") {
5772
+ this.root = root;
5773
+ }
5774
+ /** Whether this looks like a Linux `/proc` at all. */
5775
+ available() {
5776
+ return fs18.existsSync(path19.join(this.root, "stat"));
5777
+ }
5778
+ /** Every process, keyed by pid. */
5779
+ table() {
5780
+ const out = /* @__PURE__ */ new Map();
5781
+ let names;
5782
+ try {
5783
+ names = fs18.readdirSync(this.root);
5784
+ } catch {
5785
+ return out;
5786
+ }
5787
+ for (const name of names) {
5788
+ if (!/^\d+$/.test(name)) continue;
5789
+ const text = readText(path19.join(this.root, name, "stat"));
5790
+ const stat = text ? parseProcStat(Number(name), text) : null;
5791
+ if (stat) out.set(stat.pid, stat);
5792
+ }
5793
+ return out;
5794
+ }
5795
+ stat(pid) {
5796
+ const text = readText(path19.join(this.root, String(pid), "stat"));
5797
+ return text ? parseProcStat(pid, text) : null;
5798
+ }
5799
+ runWaitMs(pid) {
5800
+ const text = readText(path19.join(this.root, String(pid), "schedstat"));
5801
+ return text ? parseSchedstat(text) : null;
5802
+ }
5803
+ cmdline(pid) {
5804
+ return (readText(path19.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
5805
+ }
5806
+ /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
5807
+ statusKb(pid, key) {
5808
+ const text = readText(path19.join(this.root, String(pid), "status"));
5809
+ return text ? parseKbField(text, key) : null;
5810
+ }
5811
+ /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
5812
+ async pssKb(pid) {
5813
+ try {
5814
+ return parseKbField(await fs18.promises.readFile(path19.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
5815
+ } catch {
5816
+ return null;
5817
+ }
5818
+ }
5819
+ /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
5820
+ resetPeakRss(pid) {
5821
+ try {
5822
+ fs18.writeFileSync(path19.join(this.root, String(pid), "clear_refs"), "5");
5823
+ return true;
5824
+ } catch {
5825
+ return false;
5826
+ }
5827
+ }
5828
+ cpuTicks() {
5829
+ const text = readText(path19.join(this.root, "stat"));
5830
+ return text ? parseCpuTicks(text) : null;
5831
+ }
5832
+ /** Total stall time of a resource's pressure file, in µs. */
5833
+ pressureUs(resource) {
5834
+ const text = readText(path19.join(this.root, "pressure", resource));
5835
+ return text ? parsePsiTotal(text) : null;
5836
+ }
5837
+ memAvailableKb() {
5838
+ const text = readText(path19.join(this.root, "meminfo"));
5839
+ return text ? parseKbField(text, "MemAvailable") : null;
5840
+ }
5841
+ /** Open file descriptors of a process. */
5842
+ fdCount(pid) {
5843
+ try {
5844
+ return fs18.readdirSync(path19.join(this.root, String(pid), "fd")).length;
5845
+ } catch {
5846
+ return null;
5847
+ }
5848
+ }
5849
+ };
5850
+ var UNLIMITED = 2 ** 60;
5851
+ function numberIn(text, pattern) {
5852
+ if (text === null) return null;
5853
+ const raw = pattern ? pattern.exec(text)?.[1] : text.trim();
5854
+ const value = Number(raw);
5855
+ return raw !== void 0 && raw !== "" && Number.isFinite(value) ? value : null;
5856
+ }
5857
+ var CgroupFs = class {
5858
+ constructor(procRoot = "/proc", cgroupRoot = "/sys/fs/cgroup", pid = "self") {
5859
+ this.procRoot = procRoot;
5860
+ this.cgroupRoot = cgroupRoot;
5861
+ this.pid = pid;
5862
+ }
5863
+ /** The directory of a controller (v1) or of the unified hierarchy (v2), when it exists. */
5864
+ dirs() {
5865
+ const text = readText(path19.join(this.procRoot, String(this.pid), "cgroup"));
5866
+ if (!text) return null;
5867
+ const v1 = /* @__PURE__ */ new Map();
5868
+ let v2 = null;
5869
+ for (const line of text.split("\n")) {
5870
+ const match = /^\d+:([^:]*):(.*)$/.exec(line.trim());
5871
+ if (!match) continue;
5872
+ if (match[1] === "") v2 = match[2];
5873
+ else for (const controller of match[1].split(",")) v1.set(controller, match[2]);
5874
+ }
5875
+ const pick = (mount, sub) => {
5876
+ const nested = path19.join(mount, sub);
5877
+ if (fs18.existsSync(nested)) return nested;
5878
+ return fs18.existsSync(mount) ? mount : null;
5879
+ };
5880
+ if (v1.has("memory") || v1.has("cpu")) {
5881
+ const memoryPath = v1.get("memory");
5882
+ const cpuPath = v1.get("cpu");
5883
+ const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path19.join(this.cgroupRoot, name)).find((dir) => fs18.existsSync(dir));
5884
+ return {
5885
+ version: 1,
5886
+ memory: memoryPath !== void 0 ? pick(path19.join(this.cgroupRoot, "memory"), memoryPath) : null,
5887
+ cpu: cpuPath !== void 0 && cpuMount ? pick(cpuMount, cpuPath) : null
5888
+ };
5889
+ }
5890
+ if (v2 !== null && fs18.existsSync(path19.join(this.cgroupRoot, "cgroup.controllers"))) {
5891
+ const dir = pick(this.cgroupRoot, v2);
5892
+ return { version: 2, memory: dir, cpu: dir };
5893
+ }
5894
+ return null;
5895
+ }
5896
+ read() {
5897
+ const dirs = this.dirs();
5898
+ if (!dirs) return null;
5899
+ const file = (dir, name) => dir ? readText(path19.join(dir, name)) : null;
5900
+ if (dirs.version === 2) {
5901
+ const max = file(dirs.memory, "memory.max");
5902
+ const cpuMax = file(dirs.cpu, "cpu.max")?.trim().split(/\s+/);
5903
+ const quota = cpuMax && cpuMax[0] !== "max" ? Number(cpuMax[0]) / Number(cpuMax[1]) : null;
5904
+ const throttledUs = numberIn(file(dirs.cpu, "cpu.stat"), /^throttled_usec (\d+)/m);
5905
+ return {
5906
+ version: 2,
5907
+ memoryLimitBytes: max === null || max.trim() === "max" ? null : numberIn(max),
5908
+ memoryCurrentBytes: numberIn(file(dirs.memory, "memory.current")),
5909
+ memoryPeakBytes: numberIn(file(dirs.memory, "memory.peak")),
5910
+ oomKills: numberIn(file(dirs.memory, "memory.events"), /^oom_kill (\d+)/m),
5911
+ cpuQuotaCores: quota !== null && Number.isFinite(quota) ? quota : null,
5912
+ throttledMs: throttledUs === null ? null : throttledUs / 1e3
5913
+ };
5914
+ }
5915
+ const limit = numberIn(file(dirs.memory, "memory.limit_in_bytes"));
5916
+ const quotaUs = numberIn(file(dirs.cpu, "cpu.cfs_quota_us"));
5917
+ const periodUs = numberIn(file(dirs.cpu, "cpu.cfs_period_us"));
5918
+ const throttledNs = numberIn(file(dirs.cpu, "cpu.stat"), /^throttled_time (\d+)/m);
5919
+ return {
5920
+ version: 1,
5921
+ memoryLimitBytes: limit !== null && limit < UNLIMITED ? limit : null,
5922
+ memoryCurrentBytes: numberIn(file(dirs.memory, "memory.usage_in_bytes")),
5923
+ memoryPeakBytes: numberIn(file(dirs.memory, "memory.max_usage_in_bytes")),
5924
+ oomKills: numberIn(file(dirs.memory, "memory.oom_control"), /^oom_kill (\d+)/m),
5925
+ cpuQuotaCores: quotaUs !== null && quotaUs > 0 && periodUs ? quotaUs / periodUs : null,
5926
+ throttledMs: throttledNs === null ? null : throttledNs / 1e6
5927
+ };
5928
+ }
5929
+ };
5930
+
5931
+ // src/internal/capture/worker-metrics.ts
5932
+ var MB = 1024 * 1024;
5933
+ function round1(value) {
5934
+ return Math.round(value * 10) / 10;
5935
+ }
5936
+ var WorkerMetrics = class {
5937
+ constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
5938
+ this.proc = proc;
5939
+ this.pid = pid;
5940
+ this.loop = null;
5941
+ this.usage = null;
5942
+ /** Each process's CPU and wait at the start of the current window; null before the first. */
5943
+ this.subtree = null;
5944
+ this.roles = /* @__PURE__ */ new Map();
5945
+ this.linux = platform === "linux" && proc.available();
5946
+ let histogram = null;
5947
+ try {
5948
+ histogram = (0, import_node_perf_hooks.monitorEventLoopDelay)({ resolution: 20 });
5949
+ histogram.enable();
5950
+ } catch {
5951
+ histogram = null;
5952
+ }
5953
+ this.histogram = histogram;
5954
+ }
5955
+ /** The worker's processes under it, keyed by pid and start time. */
5956
+ tree() {
5957
+ const table = this.proc.table();
5958
+ return descendantsOf(this.pid, table.values()).map((stat) => {
5959
+ const key = `${stat.pid}:${stat.start}`;
5960
+ let role = this.roles.get(key);
5961
+ if (!role) {
5962
+ role = roleOf(stat.comm, this.proc.cmdline(stat.pid)) ?? "other";
5963
+ this.roles.set(key, role);
5964
+ }
5965
+ return { key, pid: stat.pid, cpuMs: stat.cpuMs, role };
5966
+ });
5967
+ }
5968
+ /**
5969
+ * The processes under the worker now, and the reset of their peak memory: the
5970
+ * start of the next window. Taken once per test, at its end, so the time
5971
+ * between two tests counts toward the next one.
5972
+ */
5973
+ snapshot() {
5974
+ this.subtree = /* @__PURE__ */ new Map();
5975
+ for (const proc of this.tree()) {
5976
+ this.subtree.set(proc.key, { cpuMs: proc.cpuMs, waitMs: this.proc.runWaitMs(proc.pid) });
5977
+ this.proc.resetPeakRss(proc.pid);
5978
+ }
5979
+ }
5980
+ /**
5981
+ * Start a test's window. `processes` says whether the worker has started any
5982
+ * browser to read; a worker that never did skips the process tree.
5983
+ */
5984
+ start(processes = true) {
5985
+ try {
5986
+ this.histogram?.reset();
5987
+ this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
5988
+ this.usage = process.resourceUsage();
5989
+ if (this.linux && processes && !this.subtree) this.snapshot();
5990
+ } catch {
5991
+ }
5992
+ }
5993
+ /** Close the window. Null when the test's start was never read. */
5994
+ end(processes = true) {
5995
+ if (!this.usage || !this.loop) return null;
5996
+ try {
5997
+ const usage = process.resourceUsage();
5998
+ const loop = import_node_perf_hooks.performance.eventLoopUtilization(this.loop);
5999
+ const metrics = {
6000
+ worker: {
6001
+ cpuMs: Math.round(
6002
+ (usage.userCPUTime - this.usage.userCPUTime + usage.systemCPUTime - this.usage.systemCPUTime) / 1e3
6003
+ ),
6004
+ involuntarySwitches: usage.involuntaryContextSwitches - this.usage.involuntaryContextSwitches,
6005
+ loopUtilization: Math.round(loop.utilization * 1e3) / 1e3,
6006
+ loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
6007
+ loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
6008
+ heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
6009
+ fds: this.linux ? this.proc.fdCount(this.pid) : null
6010
+ }
6011
+ };
6012
+ this.usage = null;
6013
+ this.loop = null;
6014
+ if (this.linux && processes) metrics.roles = this.roleCosts();
6015
+ return metrics;
6016
+ } catch {
6017
+ return null;
6018
+ }
6019
+ }
6020
+ /** The window's cost by role; what it reads becomes the start of the next window. */
6021
+ roleCosts() {
6022
+ const roles = {};
6023
+ const start = this.subtree ?? /* @__PURE__ */ new Map();
6024
+ const next = /* @__PURE__ */ new Map();
6025
+ for (const proc of this.tree()) {
6026
+ const from = start.get(proc.key) ?? { cpuMs: 0, waitMs: 0 };
6027
+ const waitMs = this.proc.runWaitMs(proc.pid);
6028
+ const peakKb = this.proc.statusKb(proc.pid, "VmHWM");
6029
+ const cost = roles[proc.role] ??= { cpuMs: 0, runWaitMs: null, peakRssMb: null, processes: 0 };
6030
+ cost.cpuMs += Math.max(0, proc.cpuMs - from.cpuMs);
6031
+ if (waitMs !== null && from.waitMs !== null)
6032
+ cost.runWaitMs = round1((cost.runWaitMs ?? 0) + Math.max(0, waitMs - from.waitMs));
6033
+ if (peakKb !== null) cost.peakRssMb = Math.max(cost.peakRssMb ?? 0, round1(peakKb / 1024));
6034
+ cost.processes++;
6035
+ next.set(proc.key, { cpuMs: proc.cpuMs, waitMs });
6036
+ this.proc.resetPeakRss(proc.pid);
6037
+ }
6038
+ this.subtree = next;
6039
+ return roles;
6040
+ }
6041
+ dispose() {
6042
+ this.histogram?.disable();
6043
+ }
6044
+ };
6045
+ var PageReader = class {
6046
+ constructor() {
6047
+ this.sessions = /* @__PURE__ */ new WeakMap();
6048
+ }
6049
+ async read(page) {
6050
+ let session = this.sessions.get(page);
6051
+ if (!session) {
6052
+ session = internalCall(page, async () => {
6053
+ const opened2 = await page.context().newCDPSession(page);
6054
+ await opened2.send("Performance.enable", { timeDomain: "threadTicks" });
6055
+ return opened2;
6056
+ }).catch(() => null);
6057
+ this.sessions.set(page, session);
6058
+ }
6059
+ const opened = await session;
6060
+ if (!opened) return null;
6061
+ try {
6062
+ const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
6063
+ const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
6064
+ return {
6065
+ cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
6066
+ heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
6067
+ nodes: values.get("Nodes") ?? 0,
6068
+ listeners: values.get("JSEventListeners") ?? 0
6069
+ };
6070
+ } catch {
6071
+ return null;
6072
+ }
6073
+ }
6074
+ };
6075
+
6076
+ // src/internal/capture/resource-ledger.ts
6077
+ var TRACKED_HANDLE_TYPES = ["TCPServerWrap", "FSEventWrap", "StatWatcher"];
6078
+ var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
6079
+ var CLOSE_TIMEOUT_MS = 5e3;
6080
+ var PAGE_READ_CAP = 50;
6081
+ var PAGE_READ_TIMEOUT_MS = 1e3;
6082
+ var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
6083
+ var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
6084
+ function relativePath(file) {
6085
+ return path20.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path20.sep).join("/");
6086
+ }
6087
+ function isLibraryFile(file) {
6088
+ return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path20.sep}node_modules${path20.sep}`) || file.includes("/node_modules/") || file === __filename;
6089
+ }
6090
+ function userSite(stack) {
6091
+ for (const line of (stack ?? "").split("\n").slice(1)) {
6092
+ const match = /^\s*at\s+(?:.*?\s+\()?(.+?):(\d+):\d+\)?$/.exec(line);
6093
+ if (!match) continue;
6094
+ const file = match[1].replace(/^file:\/\//, "");
6095
+ if (isLibraryFile(file) || !/\.[a-z]+$/i.test(file)) continue;
6096
+ return `${relativePath(file)}:${match[2]}`;
6097
+ }
6098
+ return null;
6099
+ }
6100
+ function zoneSite(zone) {
6101
+ for (const frame of zone?.frames ?? []) {
6102
+ if (!frame?.file || isLibraryFile(frame.file)) continue;
6103
+ return `${relativePath(frame.file)}:${frame.line ?? 0}`;
6104
+ }
6105
+ return null;
6106
+ }
6107
+ function testRef2(info) {
6108
+ if (!info?.testId) return null;
6109
+ const titlePath = Array.isArray(info.titlePath) ? info.titlePath : [];
6110
+ return {
6111
+ id: info.testId,
6112
+ file: info.file ? relativePath(info.file) : "",
6113
+ suite: titlePath.slice(1, -1).map(String)
6114
+ };
6115
+ }
6116
+ function runnableOf(info) {
6117
+ if (!info) return { phase: "worker", fixture: null };
6118
+ const runnable = info._timeoutManager?._running?.runnable;
6119
+ const type = typeof runnable?.type === "string" ? runnable.type : "test";
6120
+ const phase = HOOK_PHASES.has(type) ? type : type === "teardown" ? "worker" : "test";
6121
+ const raw = runnable?.fixture;
6122
+ const fixture = raw && typeof raw.title === "string" ? {
6123
+ title: raw.title,
6124
+ location: raw.location?.file ? `${relativePath(raw.location.file)}:${raw.location.line ?? 0}` : null,
6125
+ worker: raw.slot !== void 0 && raw.slot !== null
6126
+ } : null;
6127
+ return { phase, fixture };
6128
+ }
6129
+ function fallbackRunnable(window) {
6130
+ return { phase: window ? "test" : "worker", fixture: null };
6131
+ }
6132
+ function playwrightTestInfoSource() {
6133
+ try {
6134
+ const packageJson = resolvePlaywrightPackageJson();
6135
+ if (!packageJson) return () => null;
6136
+ const globals = (0, import_node_module2.createRequire)(packageJson)(path20.join(path20.dirname(packageJson), "lib", "globals.js"));
6137
+ const read = globals.currentTestInfo;
6138
+ if (typeof read !== "function") return () => null;
6139
+ return () => {
6140
+ try {
6141
+ return read() ?? null;
6142
+ } catch {
6143
+ return null;
6144
+ }
6145
+ };
6146
+ } catch {
6147
+ return () => null;
6148
+ }
6149
+ }
6150
+ function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
6151
+ return value === "fail" || value === "close" ? value : "report";
6152
+ }
6153
+ function countHandles() {
6154
+ const counts = {};
6155
+ const read = process.getActiveResourcesInfo;
6156
+ if (typeof read !== "function") return counts;
6157
+ for (const type of read.call(process)) {
6158
+ if (TRACKED_HANDLE_TYPES.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
6159
+ }
6160
+ return counts;
6161
+ }
6162
+ async function settledHandles(start) {
6163
+ const end = countHandles();
6164
+ if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
6165
+ await new Promise((resolve7) => setTimeout(resolve7, 0));
6166
+ return countHandles();
6167
+ }
6168
+ function settleWithin(promise, ms) {
6169
+ let timer;
6170
+ return Promise.race([
6171
+ promise.catch(() => void 0),
6172
+ new Promise((resolve7) => {
6173
+ timer = setTimeout(resolve7, ms);
6174
+ })
6175
+ ]).finally(() => clearTimeout(timer));
6176
+ }
6177
+ var ResourceLedger = class {
6178
+ constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}) {
6179
+ this.playwright = playwright;
6180
+ this.workerIndex = workerIndex;
6181
+ this.leakCheck = leakCheck;
6182
+ this.resultsFile = resultsFile;
6183
+ this.testInfoSource = testInfoSource;
6184
+ this.now = now;
6185
+ this.readers = readers;
6186
+ this.nextId = 1;
6187
+ this.entries = /* @__PURE__ */ new Map();
6188
+ this.byObject = /* @__PURE__ */ new WeakMap();
6189
+ this.pendingCalls = /* @__PURE__ */ new Set();
6190
+ this.lastCall = null;
6191
+ /** The page a wrapped page method is calling into: its first reported API call marks it used. */
6192
+ this.usingPage = null;
6193
+ this.born = [];
6194
+ this.closed = [];
6195
+ this.handlesAtStart = {};
6196
+ this.openAtStart = { contexts: 0, pages: 0 };
6197
+ /** The test between `testStarted` and `testEnded`, for when Playwright's own test info cannot be read. */
6198
+ this.window = null;
6199
+ this.listener = null;
6200
+ /** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
6201
+ this.sawBrowser = false;
6202
+ }
6203
+ /** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
6204
+ install() {
6205
+ const instrumentation = this.playwright?._instrumentation;
6206
+ if (!instrumentation || typeof instrumentation.addListener !== "function") return false;
6207
+ this.listener = {
6208
+ onApiCallBegin: (zone) => {
6209
+ if (this.pendingCalls.size > 1e3) this.pendingCalls.clear();
6210
+ this.pendingCalls.add(zone);
6211
+ this.lastCall = zone;
6212
+ if (this.usingPage) {
6213
+ this.usingPage.used = true;
6214
+ this.usingPage = null;
6215
+ }
6216
+ },
6217
+ onApiCallEnd: (zone) => {
6218
+ this.pendingCalls.delete(zone);
6219
+ },
6220
+ runAfterCreateBrowserContext: async (context) => {
6221
+ this.guard(() => this.onContext(context, userSite(new Error().stack)));
6222
+ },
6223
+ runAfterCreateRequestContext: async (request) => {
6224
+ this.guard(() => this.track(request, "request", null, userSite(new Error().stack)));
6225
+ },
6226
+ runBeforeCloseRequestContext: async (request) => {
6227
+ this.guard(() => this.onClosed(request));
6228
+ }
6229
+ };
6230
+ instrumentation.addListener(this.listener);
6231
+ for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
6232
+ if (type) this.wrapBrowserType(type);
6233
+ }
6234
+ return true;
6235
+ }
6236
+ /** Stop listening; the objects already tracked stay in the ledger. */
6237
+ uninstall() {
6238
+ const remove = this.playwright?._instrumentation?.removeListener;
6239
+ if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
6240
+ this.listener = null;
6241
+ }
6242
+ /** Mark a page used: a locator was built on it. */
6243
+ notePageUse(page) {
6244
+ const entry = this.byObject.get(page);
6245
+ if (entry) entry.used = true;
6246
+ }
6247
+ /** Open the census window of a test. */
6248
+ testStarted(info) {
6249
+ this.window = testRef2(info);
6250
+ this.handlesAtStart = countHandles();
6251
+ this.openAtStart = { contexts: 0, pages: 0 };
6252
+ for (const entry of this.openEntries()) {
6253
+ if (entry.birth.kind === "page") this.openAtStart.pages++;
6254
+ else if (entry.birth.kind === "context") this.openAtStart.contexts++;
6255
+ }
6256
+ this.readers.metrics?.start(this.sawBrowser);
6257
+ }
6258
+ /**
6259
+ * Close the census window of a test: apply `PIWI_LEAK_CHECK=close` to what the
6260
+ * test left open, then take the census. Called after the test-scoped fixtures
6261
+ * tore down, so what the test opened through them is closed by now.
6262
+ */
6263
+ async testEnded(info) {
6264
+ const test = testRef2(info) ?? this.window;
6265
+ const failed = typeof info.status === "string" && typeof info.expectedStatus === "string" && info.status !== info.expectedStatus;
6266
+ const leaks = test && !failed ? this.leaksOf(test.id) : [];
6267
+ if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
6268
+ const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
6269
+ const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
6270
+ const handles = { start: this.handlesAtStart, end: await settledHandles(this.handlesAtStart) };
6271
+ const census = this.census(ref, handles, await this.readPages());
6272
+ if (metrics) census.metrics = metrics;
6273
+ if (test) {
6274
+ census.openAtStart = this.openAtStart;
6275
+ if (!failed) census.leftOpen = leaks.length;
6276
+ }
6277
+ this.window = null;
6278
+ return { census, leaks };
6279
+ }
6280
+ /** The open pages' main thread and weight, over CDP, within a deadline. */
6281
+ async readPages() {
6282
+ const out = /* @__PURE__ */ new Map();
6283
+ const reader = this.readers.pages;
6284
+ if (!reader) return out;
6285
+ const pages = this.openEntries().filter((entry) => entry.birth.kind === "page").slice(0, PAGE_READ_CAP);
6286
+ await settleWithin(
6287
+ Promise.all(
6288
+ pages.map(async (entry) => {
6289
+ const main = entry.object ? await reader.read(entry.object) : null;
6290
+ if (main) out.set(entry.birth.id, main);
6291
+ })
6292
+ ),
6293
+ PAGE_READ_TIMEOUT_MS
6294
+ );
6295
+ return out;
6296
+ }
6297
+ /** The census a worker takes as it shuts down, appended to the results file when the reporter set one. */
6298
+ workerEnded() {
6299
+ const census = this.census(null);
6300
+ if (this.resultsFile) {
6301
+ try {
6302
+ fs19.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
6303
+ `);
6304
+ } catch {
6305
+ }
6306
+ }
6307
+ this.readers.metrics?.dispose();
6308
+ this.uninstall();
6309
+ return census;
6310
+ }
6311
+ guard(run) {
6312
+ try {
6313
+ run();
6314
+ } catch {
6315
+ }
6316
+ }
6317
+ wrapBrowserType(type) {
6318
+ for (const method of ["launch", "connect", "connectOverCDP"]) {
6319
+ const original = type[method];
6320
+ if (typeof original !== "function") continue;
6321
+ const bound = original.bind(type);
6322
+ type[method] = async (...args) => {
6323
+ const site = userSite(new Error().stack);
6324
+ const browser = await bound(...args);
6325
+ this.guard(() => this.onBrowser(browser, site));
6326
+ return browser;
6327
+ };
6328
+ }
6329
+ }
6330
+ track(object, kind, parent, site) {
6331
+ const existing = this.byObject.get(object);
6332
+ if (existing) return existing;
6333
+ const info = this.testInfoSource();
6334
+ const { phase, fixture } = info ? runnableOf(info) : fallbackRunnable(this.window);
6335
+ const birth = {
6336
+ id: this.nextId++,
6337
+ kind,
6338
+ parent: parent ? this.byObject.get(parent)?.birth.id ?? null : null,
6339
+ at: this.now(),
6340
+ phase,
6341
+ fixture,
6342
+ test: testRef2(info) ?? this.window,
6343
+ site
6344
+ };
6345
+ const entry = { birth, object, closed: null, used: false };
6346
+ if (kind === "browser" || kind === "context") this.sawBrowser = true;
6347
+ this.entries.set(birth.id, entry);
6348
+ this.byObject.set(object, entry);
6349
+ this.born.push(birth);
6350
+ return entry;
6351
+ }
6352
+ onBrowser(browser, site) {
6353
+ const known = this.byObject.get(browser);
6354
+ if (known) return known;
6355
+ const entry = this.track(browser, "browser", null, site);
6356
+ browser.on("disconnected", () => this.guard(() => this.onClosed(browser)));
6357
+ return entry;
6358
+ }
6359
+ onContext(context, site) {
6360
+ if (this.byObject.has(context)) return;
6361
+ const browser = context.browser();
6362
+ if (browser) this.onBrowser(browser, site);
6363
+ const entry = this.track(context, "context", browser, site);
6364
+ const zone = this.latestPendingCall();
6365
+ if (zone?.apiName?.endsWith(".newPage")) entry.birth.implicit = true;
6366
+ if (context._forReuse) entry.birth.reused = true;
6367
+ context.on("close", () => this.guard(() => this.onClosed(context)));
6368
+ context.on("page", (page) => this.guard(() => this.onPage(page, context)));
6369
+ for (const page of context.pages()) this.onPage(page, context, { site });
6370
+ }
6371
+ /** `initial` is set for a page that came with its context (a persistent context's), which is no popup. */
6372
+ onPage(page, context, initial) {
6373
+ if (this.byObject.has(page)) return;
6374
+ const zone = this.latestPendingCall();
6375
+ const viaNewPage = !!zone?.apiName?.endsWith(".newPage");
6376
+ const popup = !viaNewPage && !initial;
6377
+ const site = viaNewPage ? zoneSite(zone) : initial ? initial.site : zoneSite(this.lastCall);
6378
+ const entry = this.track(page, "page", context, site);
6379
+ if (popup) {
6380
+ const opener = page._opener;
6381
+ const openerEntry = opener ? this.byObject.get(opener) : void 0;
6382
+ if (openerEntry) entry.birth.opener = openerEntry.birth.id;
6383
+ if (this.lastCall?.apiName) entry.birth.trigger = this.lastCall.apiName;
6384
+ }
6385
+ const markUsed = () => {
6386
+ entry.used = true;
6387
+ };
6388
+ page.on("load", markUsed);
6389
+ page.on("domcontentloaded", markUsed);
6390
+ page.on("framenavigated", (frame) => {
6391
+ try {
6392
+ if (frame === page.mainFrame() && frame.url() !== "about:blank") entry.used = true;
6393
+ } catch {
6394
+ }
6395
+ });
6396
+ page.on("close", () => this.guard(() => this.onClosed(page)));
6397
+ const methods = page;
6398
+ for (const method of USE_METHODS) {
6399
+ const original = methods[method];
6400
+ if (typeof original !== "function") continue;
6401
+ const bound = original.bind(page);
6402
+ methods[method] = (...args) => {
6403
+ const previous = this.usingPage;
6404
+ this.usingPage = entry;
6405
+ try {
6406
+ return bound(...args);
6407
+ } finally {
6408
+ this.usingPage = previous;
6409
+ }
6410
+ };
6411
+ }
6412
+ }
6413
+ onClosed(object, byPiwi = false) {
6414
+ const entry = this.byObject.get(object);
6415
+ if (!entry || entry.closed) return;
6416
+ const info = this.testInfoSource();
6417
+ const { phase } = info ? runnableOf(info) : fallbackRunnable(this.window);
6418
+ const close = {
6419
+ id: entry.birth.id,
6420
+ at: this.now(),
6421
+ phase,
6422
+ test: testRef2(info) ?? this.window
6423
+ };
6424
+ if (entry.birth.kind === "page") close.used = entry.used;
6425
+ if (byPiwi) close.byPiwi = true;
6426
+ entry.closed = close;
6427
+ entry.object = null;
6428
+ this.closed.push(close);
6429
+ }
6430
+ latestPendingCall() {
6431
+ let latest = null;
6432
+ for (const zone of this.pendingCalls) latest = zone;
6433
+ return latest;
6434
+ }
6435
+ openEntries() {
6436
+ return [...this.entries.values()].filter((entry) => !entry.closed && entry.object);
6437
+ }
6438
+ /**
6439
+ * Objects a test opened itself, by its body or its each-hooks, and left open.
6440
+ * A page in a leaked context, or a context in a leaked browser, goes with it;
6441
+ * the context `browser.newPage()` opened for its page goes with the page.
6442
+ */
6443
+ leaksOf(testId) {
6444
+ const open = this.openEntries().filter(
6445
+ (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
6446
+ );
6447
+ const openIds = new Set(open.map((entry) => entry.birth.id));
6448
+ const foldsIntoLeak = (entry) => {
6449
+ let parentId = entry.birth.parent;
6450
+ while (parentId !== null) {
6451
+ if (!this.isImplicit(parentId)) return openIds.has(parentId);
6452
+ parentId = this.entries.get(parentId)?.birth.parent ?? null;
6453
+ }
6454
+ return false;
6455
+ };
6456
+ return open.filter((entry) => !(entry.birth.kind === "context" && this.isImplicit(entry.birth.id))).filter((entry) => !foldsIntoLeak(entry)).map((entry) => ({
6457
+ id: entry.birth.id,
6458
+ kind: entry.birth.kind,
6459
+ site: entry.birth.site,
6460
+ pages: entry.birth.kind === "page" ? 0 : this.pagesUnder(entry.birth.id),
6461
+ implicit: false,
6462
+ ...entry.birth.opener !== void 0 ? { trigger: entry.birth.trigger ?? "a click" } : {}
6463
+ })).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
6464
+ }
6465
+ /** Open pages under a context, or under any context of a browser. */
6466
+ pagesUnder(id) {
6467
+ let count = 0;
6468
+ for (const entry of this.openEntries()) {
6469
+ if (entry.birth.kind !== "page" || entry.birth.parent === null) continue;
6470
+ const context = this.entries.get(entry.birth.parent);
6471
+ if (entry.birth.parent === id || context?.birth.parent === id) count++;
6472
+ }
6473
+ return count;
6474
+ }
6475
+ isImplicit(id) {
6476
+ const entry = this.entries.get(id);
6477
+ if (!entry || entry.birth.kind !== "context") return false;
6478
+ if (entry.birth.implicit) return true;
6479
+ const owner = entry.object ? entry.object._ownerPage : void 0;
6480
+ if (owner) entry.birth.implicit = true;
6481
+ return !!owner;
6482
+ }
6483
+ openPagesOf(contextId) {
6484
+ let count = 0;
6485
+ for (const entry of this.openEntries())
6486
+ if (entry.birth.kind === "page" && entry.birth.parent === contextId) count++;
6487
+ return count;
6488
+ }
6489
+ async closeLeaks(leaks) {
6490
+ for (const leak of leaks) {
6491
+ const entry = this.entries.get(leak.id);
6492
+ const object = entry?.object;
6493
+ if (!entry || entry.closed || !object) continue;
6494
+ const close = leak.kind === "request" ? () => object.dispose() : () => object.close();
6495
+ await settleWithin(internalCall(object, close), CLOSE_TIMEOUT_MS);
6496
+ const closedMeanwhile = entry.closed;
6497
+ if (closedMeanwhile) closedMeanwhile.byPiwi = true;
6498
+ else this.onClosed(object, true);
6499
+ }
6500
+ }
6501
+ census(test, handles, mains = /* @__PURE__ */ new Map()) {
6502
+ const open = [];
6503
+ for (const entry of this.openEntries()) {
6504
+ const object = entry.object;
6505
+ if (entry.birth.kind === "page") {
6506
+ const main = mains.get(entry.birth.id);
6507
+ open.push({
6508
+ id: entry.birth.id,
6509
+ used: entry.used,
6510
+ url: this.pageUrl(object),
6511
+ listeners: this.listenerCount(object),
6512
+ routes: this.routeCount(object),
6513
+ ...main ? { main } : {}
6514
+ });
6515
+ } else if (entry.birth.kind === "context") {
6516
+ this.isImplicit(entry.birth.id);
6517
+ open.push({ id: entry.birth.id, pages: this.openPagesOf(entry.birth.id), routes: this.routeCount(object) });
6518
+ } else {
6519
+ open.push({ id: entry.birth.id });
6520
+ }
6521
+ }
6522
+ const census = {
6523
+ v: 1,
6524
+ worker: this.workerIndex,
6525
+ pid: process.pid,
6526
+ at: this.now(),
6527
+ test,
6528
+ born: this.born,
6529
+ closed: this.closed,
6530
+ open
6531
+ };
6532
+ if (handles) census.handles = handles;
6533
+ this.born = [];
6534
+ this.closed = [];
6535
+ return census;
6536
+ }
6537
+ pageUrl(page) {
6538
+ try {
6539
+ const url = typeof page.url === "function" ? String(page.url()) : "";
6540
+ return url ? url.split(/[?#]/)[0] : void 0;
6541
+ } catch {
6542
+ return void 0;
6543
+ }
6544
+ }
6545
+ listenerCount(emitter) {
6546
+ try {
6547
+ if (typeof emitter.eventNames !== "function" || typeof emitter.listenerCount !== "function") return void 0;
6548
+ const names = emitter.eventNames();
6549
+ return names.reduce((sum, name) => sum + emitter.listenerCount(name), 0);
6550
+ } catch {
6551
+ return void 0;
6552
+ }
6553
+ }
6554
+ routeCount(object) {
6555
+ const routes = object._routes;
6556
+ return Array.isArray(routes) ? routes.length : void 0;
6557
+ }
6558
+ };
6559
+ var activeLedger = null;
6560
+ function startResourceLedger(playwright, workerIndex) {
6561
+ if (process.env[PIWI_ENV_KEYS.captureResources] === "false") return null;
6562
+ if (activeLedger) return activeLedger;
6563
+ const metrics = new WorkerMetrics();
6564
+ const ledger = new ResourceLedger(
6565
+ playwright,
6566
+ workerIndex,
6567
+ readLeakCheck(),
6568
+ process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
6569
+ playwrightTestInfoSource(),
6570
+ Date.now,
6571
+ { metrics, pages: new PageReader() }
6572
+ );
6573
+ if (!ledger.install()) {
6574
+ metrics.dispose();
6575
+ return null;
6576
+ }
6577
+ activeLedger = ledger;
6578
+ return ledger;
6579
+ }
6580
+ function activeResourceLedger() {
6581
+ return activeLedger;
6582
+ }
6583
+ function stopResourceLedger() {
6584
+ const ledger = activeLedger;
6585
+ activeLedger = null;
6586
+ return ledger ? ledger.workerEnded() : null;
6587
+ }
6588
+ function noteResourceUse(page) {
6589
+ activeLedger?.notePageUse(page);
6590
+ }
6591
+ var ARTICLE = {
6592
+ browser: "a browser",
6593
+ context: "a context",
6594
+ page: "a page",
6595
+ request: "an API request context"
6596
+ };
6597
+ function leakFailureMessage(leaks) {
6598
+ const lines = leaks.map((leak) => {
6599
+ const what = leak.trigger ? `a popup opened by ${leak.trigger}` : ARTICLE[leak.kind];
6600
+ const where = leak.site ? `${leak.trigger ? "" : " opened"} at ${leak.site}` : "";
6601
+ const pages = leak.pages > 0 ? `, with ${leak.pages} page${leak.pages === 1 ? "" : "s"}` : "";
6602
+ return ` - ${what}${where}${pages}`;
6603
+ });
6604
+ return [
6605
+ `Piwi: this test left ${leaks.length === 1 ? "one object" : `${leaks.length} objects`} open:`,
6606
+ ...lines,
6607
+ "Close each before the test ends (await using, close() or dispose()), or set PIWI_LEAK_CHECK=report to only list them."
6608
+ ].join("\n");
6609
+ }
6610
+ async function recordResourceCensus(testInfo) {
6611
+ const ledger = activeLedger;
6612
+ if (!ledger) return;
6613
+ const { census, leaks } = await ledger.testEnded(testInfo);
6614
+ try {
6615
+ await testInfo.attach(ATTACHMENT_NAMES.resources, {
6616
+ contentType: "application/json",
6617
+ body: Buffer.from(JSON.stringify(census))
6618
+ });
6619
+ } catch {
6620
+ }
6621
+ if (ledger.leakCheck === "fail" && leaks.length > 0) throw new Error(leakFailureMessage(leaks));
6622
+ }
6623
+
6624
+ // src/internal/collect/resource-verdicts.ts
6625
+ var path21 = __toESM(require("path"));
6626
+ var OPEN_STEP_TITLES = {
6627
+ "Create context": "context",
6628
+ "Launch persistent context": "context",
6629
+ "Launch browser": "browser",
6630
+ "Connect over CDP": "browser"
6631
+ };
6632
+ var CLOSE_STEP_TITLES = {
6633
+ "Close context": "context",
6634
+ "Close browser": "browser"
6635
+ };
6636
+ function parseResourceCensus(body) {
6637
+ if (!body) return null;
6638
+ try {
6639
+ const value = JSON.parse(body.toString());
6640
+ if (value?.v !== 1 || !Array.isArray(value.born) || !Array.isArray(value.closed) || !Array.isArray(value.open)) {
6641
+ return null;
6642
+ }
6643
+ if (typeof value.worker !== "number" || typeof value.at !== "number") return null;
6644
+ return value;
6645
+ } catch {
6646
+ return null;
6647
+ }
6648
+ }
6649
+ function parseResourceResults(text) {
6650
+ return text.split("\n").map((line) => line.trim() ? parseResourceCensus(line) : null).filter((census) => census !== null);
6651
+ }
6652
+ function userFixturesOf(steps) {
6653
+ const out = [];
6654
+ const walk = (list) => {
6655
+ for (const step of list) {
6656
+ const file = step.location?.file;
6657
+ if (step.category === "fixture" && file && !file.includes(`${path21.sep}node_modules${path21.sep}`)) {
6658
+ const name = step.title.replace(/^(?:Fixture\s+|fixture:\s*)/, "").replace(/^"(.*)"$/, "$1");
6659
+ if (!name.startsWith("piwi")) out.push(`${name} (${relativeFile(file)}:${step.location.line})`);
6660
+ }
6661
+ walk(step.steps ?? []);
6662
+ }
6663
+ };
6664
+ walk(steps);
6665
+ return [...new Set(out)];
6666
+ }
6667
+ function tallyLifecycleSteps(file, steps) {
6668
+ const tally = { file, opened: [], closed: { context: 0, browser: 0 } };
6669
+ const walk = (list) => {
6670
+ for (const step of list) {
6671
+ if (step.category === "fixture") continue;
6672
+ if (step.category === "pw:api") {
6673
+ const opened = OPEN_STEP_TITLES[step.title];
6674
+ const closed = CLOSE_STEP_TITLES[step.title];
6675
+ if (opened) {
6676
+ tally.opened.push({
6677
+ kind: opened,
6678
+ site: step.location ? `${relativeFile(step.location.file)}:${step.location.line}` : null
6679
+ });
6680
+ }
6681
+ if (closed) tally.closed[closed]++;
6682
+ }
6683
+ walk(step.steps ?? []);
6684
+ }
6685
+ };
6686
+ walk(steps);
6687
+ return tally;
6688
+ }
6689
+ function relativeFile(file) {
6690
+ return path21.relative(process.cwd(), file).split(path21.sep).join("/");
6691
+ }
6692
+ function buildResourceReport(input) {
6693
+ const findings = [];
6694
+ const byWorker = /* @__PURE__ */ new Map();
6695
+ for (const census of input.censuses) {
6696
+ const list = byWorker.get(census.worker) ?? [];
6697
+ list.push(census);
6698
+ byWorker.set(census.worker, list);
6699
+ }
6700
+ const leaked = [];
6701
+ const idle = [];
6702
+ const all = [];
6703
+ for (const censuses of byWorker.values()) {
6704
+ censuses.sort((a, b) => a.at - b.at);
6705
+ const tracked = /* @__PURE__ */ new Map();
6706
+ for (const census of censuses) {
6707
+ for (const birth of census.born) {
6708
+ tracked.set(birth.id, {
6709
+ worker: census.worker,
6710
+ birth,
6711
+ bornCensus: census,
6712
+ close: null,
6713
+ lastOpenAt: null,
6714
+ used: void 0,
6715
+ series: [],
6716
+ mainCpu: []
6717
+ });
6718
+ }
6719
+ for (const close of census.closed) {
6720
+ const entry = tracked.get(close.id);
6721
+ if (!entry) continue;
6722
+ entry.close = close;
6723
+ if (close.used !== void 0) entry.used = close.used;
6724
+ }
6725
+ for (const open of census.open) {
6726
+ const entry = tracked.get(open.id);
6727
+ if (!entry) continue;
6728
+ entry.lastOpenAt = census.at;
6729
+ if (open.used !== void 0) entry.used = open.used;
6730
+ if (census.test) entry.series.push({ pages: open.pages, listeners: open.listeners, routes: open.routes });
6731
+ if (open.main) entry.mainCpu.push({ at: census.at, cpuMs: open.main.cpuMs });
6732
+ }
6733
+ }
6734
+ const testCensuses = censuses.filter((census) => census.test !== null);
6735
+ const workerEnded = censuses.some((census) => census.test === null);
6736
+ for (const entry of tracked.values()) {
6737
+ all.push(entry);
6738
+ if (entry.birth.kind === "page" && entry.used === false && !entry.bornCensus.test?.failed) idle.push(entry);
6739
+ const verdict = leakVerdict(entry, testCensuses, workerEnded);
6740
+ if (verdict) leaked.push({ tracked: entry, ...verdict });
6741
+ }
6742
+ }
6743
+ findings.push(...groupLeaks(leaked, all));
6744
+ findings.push(...pilingFindings(all));
6745
+ findings.push(...handleFindings(byWorker));
6746
+ findings.push(...groupIdle(idle, input.fixturesByTest ?? /* @__PURE__ */ new Map()));
6747
+ findings.push(...probableFindings(input.tallies ?? []));
6748
+ const counts = { leaked: 0, idle: 0, piling: 0, handle: 0, probable: 0 };
6749
+ for (const finding of findings) counts[finding.verdict] += finding.verdict === "idle" ? finding.count : 1;
6750
+ return { findings, counts };
6751
+ }
6752
+ function sameFile(birth, ref) {
6753
+ return !!ref && !!birth.test && ref.file === birth.test.file;
6754
+ }
6755
+ function insideBlock(birth, ref) {
6756
+ if (!ref || !sameFile(birth, ref)) return false;
6757
+ return birth.test.suite.every((title, index) => ref.suite[index] === title);
6758
+ }
6759
+ function leakVerdict(entry, testCensuses, workerEnded) {
6760
+ const { birth, close, bornCensus } = entry;
6761
+ if (birth.reused || birth.phase === "worker" || birth.fixture?.worker || !birth.test) return null;
6762
+ if (birth.kind === "context" && birth.implicit) return null;
6763
+ if (close?.byPiwi) return { scope: "test", heldMs: 0, untilWorkerEnd: false };
6764
+ const closedByShutdown = !!close && (close.phase === "worker" || close.test === null);
6765
+ if (birth.phase === "beforeAll" || birth.phase === "afterAll") {
6766
+ if (close && !closedByShutdown && sameFile(birth, close.test)) return null;
6767
+ const ended = workerEnded || testCensuses.some((census) => census.at >= bornCensus.at && !insideBlock(birth, census.test));
6768
+ if (!ended) return null;
6769
+ const lastInside = [...testCensuses].reverse().find((census) => insideBlock(birth, census.test));
6770
+ const blockEnd = lastInside?.at ?? bornCensus.at;
6771
+ const end2 = close?.at ?? entry.lastOpenAt ?? blockEnd;
6772
+ return { scope: "describe", heldMs: Math.max(0, end2 - blockEnd), untilWorkerEnd: !close || closedByShutdown };
6773
+ }
6774
+ if (bornCensus.test === null || bornCensus.test.id !== birth.test.id) return null;
6775
+ if (bornCensus.test.failed) return null;
6776
+ const openAtItsEnd = bornCensus.open.some((open) => open.id === birth.id);
6777
+ if (!openAtItsEnd) return null;
6778
+ if (close && !closedByShutdown && (close.test?.id === birth.test.id || sameFile(birth, close.test))) return null;
6779
+ const end = close?.at ?? entry.lastOpenAt ?? bornCensus.at;
6780
+ return { scope: "test", heldMs: Math.max(0, end - bornCensus.at), untilWorkerEnd: !close || closedByShutdown };
6781
+ }
6782
+ function whereOf(birth) {
6783
+ if (birth.opener !== void 0) {
6784
+ return `popup${birth.trigger ? ` after ${birth.trigger}` : ""}${birth.site ? ` at ${birth.site}` : ""}`;
6785
+ }
6786
+ if (birth.fixture) {
6787
+ const at = birth.site ?? birth.fixture.location;
6788
+ return `fixture "${birth.fixture.title}"${at ? ` at ${at}` : ""}`;
6789
+ }
6790
+ return birth.site ?? "an unknown line";
6791
+ }
6792
+ function afterTestCpu(entry) {
6793
+ const readings = entry.mainCpu;
6794
+ if (readings.length < 2) return 0;
6795
+ const first = readings.find((reading) => reading.at >= entry.bornCensus.at) ?? readings[0];
6796
+ return Math.max(0, readings[readings.length - 1].cpuMs - first.cpuMs);
6797
+ }
6798
+ function siteOf(birth) {
6799
+ return birth.site ?? birth.fixture?.location ?? null;
6800
+ }
6801
+ var keyOf = (worker, id) => `${worker}:${id}`;
6802
+ function groupLeaks(leaked, all) {
6803
+ const leaks = new Map(leaked.map((leak) => [keyOf(leak.tracked.worker, leak.tracked.birth.id), leak]));
6804
+ const tracked = new Map(all.map((entry) => [keyOf(entry.worker, entry.birth.id), entry]));
6805
+ const groups = /* @__PURE__ */ new Map();
6806
+ const groupKey = (leak) => `${leak.scope}|${leak.tracked.birth.kind}|${whereOf(leak.tracked.birth)}`;
6807
+ for (const leak of leaked) {
6808
+ const { birth } = leak.tracked;
6809
+ if (birth.kind === "context" && birth.implicit) continue;
6810
+ if (foldTarget(leak.tracked, leaks, tracked)) continue;
6811
+ const key = groupKey(leak);
6812
+ const group = groups.get(key) ?? {
6813
+ verdict: "leaked",
6814
+ kind: birth.kind,
6815
+ where: whereOf(birth),
6816
+ site: siteOf(birth),
6817
+ scope: leak.scope,
6818
+ tests: 0,
6819
+ count: 0,
6820
+ heldMs: 0,
6821
+ untilWorkerEnd: false,
6822
+ pages: 0,
6823
+ testIds: /* @__PURE__ */ new Set(),
6824
+ ...leak.scope === "describe" && birth.test?.suite.length ? { detail: `beforeAll of "${birth.test.suite.join(" \u203A ")}"` } : {}
6825
+ };
6826
+ group.count++;
6827
+ if (birth.test) group.testIds.add(birth.test.id);
6828
+ group.heldMs = Math.max(group.heldMs ?? 0, leak.heldMs);
6829
+ group.untilWorkerEnd = group.untilWorkerEnd || leak.untilWorkerEnd;
6830
+ if (leak.tracked.close?.byPiwi) group.closedByPiwi = true;
6831
+ if (birth.kind === "page" && leak.scope === "test") addCpu(group, afterTestCpu(leak.tracked));
6832
+ groups.set(key, group);
6833
+ }
6834
+ for (const leak of leaked) {
6835
+ if (leak.tracked.birth.kind !== "page") continue;
6836
+ const root = foldTarget(leak.tracked, leaks, tracked);
6837
+ const group = root ? groups.get(groupKey(root)) : void 0;
6838
+ if (!group) continue;
6839
+ group.pages = (group.pages ?? 0) + 1;
6840
+ if (root.scope === "test") addCpu(group, afterTestCpu(leak.tracked));
6841
+ }
6842
+ return [...groups.values()].map(({ testIds, ...finding }) => ({ ...finding, tests: testIds.size })).sort((a, b) => a.scope === b.scope ? b.count - a.count : a.scope === "test" ? -1 : 1);
6843
+ }
6844
+ function addCpu(group, cpuMs) {
6845
+ if (cpuMs > 0) group.afterTestCpuMs = (group.afterTestCpuMs ?? 0) + cpuMs;
6846
+ }
6847
+ function foldTarget(entry, leaks, tracked) {
6848
+ let parentId = entry.birth.parent;
6849
+ while (parentId !== null) {
6850
+ const key = keyOf(entry.worker, parentId);
6851
+ const parent = tracked.get(key);
6852
+ if (!parent) return null;
6853
+ if (!(parent.birth.kind === "context" && parent.birth.implicit)) return leaks.get(key) ?? null;
6854
+ parentId = parent.birth.parent;
6855
+ }
6856
+ return null;
6857
+ }
6858
+ function groupIdle(idle, fixturesByTest) {
6859
+ const groups = /* @__PURE__ */ new Map();
6860
+ for (const entry of idle) {
6861
+ const { birth } = entry;
6862
+ const fixtures = birth.test ? fixturesByTest.get(birth.test.id) ?? [] : [];
6863
+ const detail = birth.fixture && fixtures.length ? `set up with ${fixtures.join(", ")}` : void 0;
6864
+ const where = whereOf(birth);
6865
+ const key = `${where}|${detail ?? ""}`;
6866
+ const group = groups.get(key) ?? {
6867
+ verdict: "idle",
6868
+ kind: "page",
6869
+ where,
6870
+ site: siteOf(birth),
6871
+ tests: 0,
6872
+ count: 0,
6873
+ testIds: /* @__PURE__ */ new Set(),
6874
+ ...detail ? { detail } : {}
6875
+ };
6876
+ group.count++;
6877
+ if (birth.test) group.testIds.add(birth.test.id);
6878
+ groups.set(key, group);
6879
+ }
6880
+ return [...groups.values()].map(({ testIds, ...finding }) => ({ ...finding, tests: testIds.size })).sort((a, b) => b.count - a.count);
6881
+ }
6882
+ function pilingFindings(all) {
6883
+ const out = [];
6884
+ for (const entry of all) {
6885
+ for (const what of ["pages", "listeners", "routes"]) {
6886
+ const values = entry.series.map((point) => point[what]).filter((v) => typeof v === "number");
6887
+ if (values.length < 3) continue;
6888
+ const rising = values.every((value, i) => i === 0 || value >= values[i - 1]);
6889
+ const from = values[0];
6890
+ const to = values[values.length - 1];
6891
+ if (!rising || to - from < 2) continue;
6892
+ out.push({
6893
+ verdict: "piling",
6894
+ kind: entry.birth.kind,
6895
+ where: whereOf(entry.birth),
6896
+ site: siteOf(entry.birth),
6897
+ tests: values.length,
6898
+ count: 1,
6899
+ growth: { what, from, to, tests: values.length }
6900
+ });
6901
+ }
6902
+ }
6903
+ return out.sort((a, b) => b.growth.to - b.growth.from - (a.growth.to - a.growth.from));
6904
+ }
6905
+ function handleFindings(byWorker) {
6906
+ const groups = /* @__PURE__ */ new Map();
6907
+ for (const censuses of byWorker.values()) {
6908
+ const ordered = censuses.filter((census) => census.test && census.handles).sort((a, b) => a.at - b.at);
6909
+ for (let i = 0; i < ordered.length - 1; i++) {
6910
+ const census = ordered[i];
6911
+ const next = ordered[i + 1];
6912
+ for (const [type, end] of Object.entries(census.handles.end)) {
6913
+ const grew = end - (census.handles.start[type] ?? 0);
6914
+ if (grew <= 0 || (next.handles.start[type] ?? 0) < end) continue;
6915
+ const test = census.test;
6916
+ const key = `${test.id}|${type}`;
6917
+ const group = groups.get(key) ?? {
6918
+ verdict: "handle",
6919
+ kind: "handle",
6920
+ where: type,
6921
+ tests: 1,
6922
+ count: 0,
6923
+ detail: `"${test.title ?? ""}" (${test.file})`
6924
+ };
6925
+ group.count += grew;
6926
+ groups.set(key, group);
6927
+ }
6928
+ }
6929
+ }
6930
+ return [...groups.values()];
6931
+ }
6932
+ function probableFindings(tallies) {
6933
+ const byFile = /* @__PURE__ */ new Map();
6934
+ for (const tally of tallies) {
6935
+ const merged = byFile.get(tally.file) ?? { file: tally.file, opened: [], closed: { context: 0, browser: 0 } };
6936
+ merged.opened.push(...tally.opened);
6937
+ merged.closed.context += tally.closed.context;
6938
+ merged.closed.browser += tally.closed.browser;
6939
+ byFile.set(tally.file, merged);
6940
+ }
6941
+ const out = [];
6942
+ for (const tally of byFile.values()) {
6943
+ for (const kind of ["browser", "context"]) {
6944
+ const opened = tally.opened.filter((o) => o.kind === kind);
6945
+ if (opened.length <= tally.closed[kind]) continue;
6946
+ const sites = [...new Set(opened.map((o) => o.site ?? tally.file))];
6947
+ out.push({
6948
+ verdict: "probable",
6949
+ kind,
6950
+ where: sites.join(", "),
6951
+ tests: 0,
6952
+ count: opened.length - tally.closed[kind],
6953
+ detail: `${opened.length} opened, ${tally.closed[kind]} closed in ${tally.file}`
6954
+ });
6955
+ }
6956
+ }
6957
+ return out;
6958
+ }
6959
+ function formatHeld(ms) {
6960
+ if (ms < 1e3) return `${Math.round(ms)} ms`;
6961
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)} s`;
6962
+ const minutes = Math.floor(ms / 6e4);
6963
+ const seconds = Math.round(ms % 6e4 / 1e3);
6964
+ return seconds ? `${minutes} min ${seconds} s` : `${minutes} min`;
6965
+ }
6966
+ var KIND_LABEL = {
6967
+ browser: "browser",
6968
+ context: "context",
6969
+ page: "page",
6970
+ request: "API context",
6971
+ handle: "handle"
6972
+ };
6973
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
6974
+ var HANDLE_LABEL = {
6975
+ TCPServerWrap: "server",
6976
+ FSEventWrap: "file watcher",
6977
+ StatWatcher: "file watcher"
6978
+ };
6979
+ function findingLine(finding) {
6980
+ const verdict = finding.verdict.padEnd(8);
6981
+ const label = finding.verdict === "handle" ? HANDLE_LABEL[finding.where] ?? finding.where : finding.growth ? finding.growth.what : KIND_LABEL[finding.kind];
6982
+ const kind = label.padEnd(11);
6983
+ const parts = finding.verdict === "handle" ? [] : [finding.where];
6984
+ switch (finding.verdict) {
6985
+ case "leaked": {
6986
+ if (finding.count > 1) parts.push(`${finding.count} ${KIND_LABEL[finding.kind]}s`);
6987
+ if (finding.detail) parts.push(finding.detail);
6988
+ if (finding.pages) parts.push(`with ${plural(finding.pages, "page")}`);
6989
+ if (finding.tests) parts.push(plural(finding.tests, "test"));
6990
+ const past = finding.scope === "describe" ? "its describe block" : "its test";
6991
+ if (finding.closedByPiwi) parts.push("closed by Piwi at the end of its test");
6992
+ else if (finding.untilWorkerEnd)
6993
+ parts.push(`open until the worker shut down (${formatHeld(finding.heldMs ?? 0)} past ${past})`);
6994
+ else parts.push(`open ${formatHeld(finding.heldMs ?? 0)} past ${past}`);
6995
+ if ((finding.afterTestCpuMs ?? 0) >= 500)
6996
+ parts.push(`${formatHeld(finding.afterTestCpuMs)} of page CPU after its test`);
6997
+ break;
6998
+ }
6999
+ case "idle":
7000
+ parts.push(plural(finding.count, "page"));
7001
+ if (finding.tests) parts.push(plural(finding.tests, "test"));
7002
+ if (finding.detail) parts.push(finding.detail);
7003
+ break;
7004
+ case "piling": {
7005
+ const growth = finding.growth;
7006
+ const noun = growth.what === "pages" ? "open pages" : growth.what === "routes" ? "route handlers" : "listeners";
7007
+ parts.push(`${growth.from} \u2192 ${growth.to} ${noun} over ${growth.tests} tests`);
7008
+ break;
7009
+ }
7010
+ case "handle":
7011
+ parts.push(`left running in the worker by ${finding.detail ?? "a test"}`);
7012
+ break;
7013
+ case "probable":
7014
+ if (finding.detail) parts.push(finding.detail);
7015
+ break;
7016
+ }
7017
+ return ` ${verdict} ${kind} ${parts.join(" \xB7 ")}`;
7018
+ }
7019
+ function formatResourceSummary(report, leakCheck, maxLines = 10) {
7020
+ if (report.findings.length === 0) return [];
7021
+ const { counts } = report;
7022
+ const head = [
7023
+ counts.leaked ? plural(counts.leaked, "leak") : "",
7024
+ counts.idle ? `${plural(counts.idle, "idle page")}` : "",
7025
+ counts.piling ? `${counts.piling} piling up` : "",
7026
+ counts.handle ? plural(counts.handle, "handle") + " left in workers" : "",
7027
+ counts.probable ? plural(counts.probable, "probable leak") : ""
7028
+ ].filter(Boolean);
7029
+ const lines = [`Resources: ${head.join(" \xB7 ")}`];
7030
+ for (const finding of report.findings.slice(0, maxLines)) lines.push(findingLine(finding));
7031
+ const rest = report.findings.length - maxLines;
7032
+ if (rest > 0) lines.push(` \u2026 and ${plural(rest, "more finding")}`);
7033
+ const testLeaks = report.findings.some((f) => f.verdict === "leaked" && f.scope === "test" && !f.closedByPiwi);
7034
+ if (testLeaks && leakCheck === "report") {
7035
+ lines.push(
7036
+ "Set leakCheck: 'fail' (PIWI_LEAK_CHECK=fail) to fail the tests that leave what they open, or 'close' to close it."
7037
+ );
7038
+ }
7039
+ if (counts.probable) {
7040
+ lines.push(
7041
+ "Probable leaks are counted from the steps alone; the capture fixtures (extendPiwiFixtures) make them exact."
7042
+ );
7043
+ }
7044
+ return lines;
7045
+ }
7046
+
7047
+ // src/internal/collect/process-sampler.ts
7048
+ var fs20 = __toESM(require("fs"));
7049
+ var os8 = __toESM(require("os"));
7050
+ var path22 = __toESM(require("path"));
7051
+ var import_node_child_process2 = require("child_process");
7052
+ var WALK_FILE_CAP = 1e4;
7053
+ var SERIES_CAP = 3600;
7054
+ var TIMED_CAP = 3600;
7055
+ var PLAYWRIGHT_TEMP = /^(playwright_\w+dev_profile-|playwright-artifacts-)/;
7056
+ function round12(value) {
7057
+ return Math.round(value * 10) / 10;
7058
+ }
7059
+ function mean(a, b) {
7060
+ return round12((a + b) / 2);
7061
+ }
7062
+ function mean2(a, b) {
7063
+ return Math.round((a + b) / 2 * 100) / 100;
7064
+ }
7065
+ function pushTimed(points, point, merge) {
7066
+ points.push(point);
7067
+ if (points.length <= TIMED_CAP) return points;
7068
+ const merged = [];
7069
+ for (let i = 0; i < points.length; i += 2) {
7070
+ const next = points[i + 1];
7071
+ merged.push(next ? [points[i][0], merge(points[i][1], next[1])] : points[i]);
7072
+ }
7073
+ return merged;
7074
+ }
7075
+ function pct(part, whole) {
7076
+ return whole > 0 ? round12(part / whole * 100) : null;
7077
+ }
7078
+ function nodeCpuTicks() {
7079
+ const cpus2 = os8.cpus();
7080
+ if (cpus2.length === 0) return null;
7081
+ let busy = 0;
7082
+ let idle = 0;
7083
+ for (const cpu of cpus2) {
7084
+ busy += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq;
7085
+ idle += cpu.times.idle;
7086
+ }
7087
+ return { busy, idle, iowait: 0, steal: 0, total: busy + idle };
7088
+ }
7089
+ function defaultPs() {
7090
+ return new Promise((resolve7, reject) => {
7091
+ (0, import_node_child_process2.execFile)(
7092
+ "ps",
7093
+ ["-A", "-o", "pid=,ppid=,rss=,time=,command="],
7094
+ { timeout: 2e3, maxBuffer: 8 * 1024 * 1024 },
7095
+ (error, stdout) => error ? reject(error) : resolve7(stdout)
7096
+ );
7097
+ });
7098
+ }
7099
+ function defaultWindowsQuery() {
7100
+ return new Promise((resolve7, reject) => {
7101
+ (0, import_node_child_process2.execFile)(
7102
+ "powershell.exe",
7103
+ [
7104
+ "-NoProfile",
7105
+ "-NonInteractive",
7106
+ "-Command",
7107
+ "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $PID -and $_.ParentProcessId -ne $PID } | Select-Object ProcessId,ParentProcessId,WorkingSetSize,UserModeTime,KernelModeTime,Name,CommandLine | ConvertTo-Json -Compress"
7108
+ ],
7109
+ { timeout: 1e4, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
7110
+ (error, stdout) => error ? reject(error) : resolve7(stdout)
7111
+ );
7112
+ });
7113
+ }
7114
+ function freeBytes(dir) {
7115
+ let current = path22.resolve(dir);
7116
+ for (; ; ) {
7117
+ try {
7118
+ const stats = fs20.statfsSync(current);
7119
+ return stats.bavail * stats.bsize;
7120
+ } catch {
7121
+ const parent = path22.dirname(current);
7122
+ if (parent === current) return null;
7123
+ current = parent;
7124
+ }
7125
+ }
7126
+ }
7127
+ async function dirBytes(dir, budget) {
7128
+ let bytes = 0;
7129
+ let entries;
7130
+ try {
7131
+ entries = await fs20.promises.readdir(dir, { withFileTypes: true });
7132
+ } catch {
7133
+ return 0;
7134
+ }
7135
+ for (const entry of entries) {
7136
+ if (budget.files <= 0) return bytes;
7137
+ const full = path22.join(dir, entry.name);
7138
+ if (entry.isDirectory()) bytes += await dirBytes(full, budget);
7139
+ else if (entry.isFile()) {
7140
+ budget.files--;
7141
+ try {
7142
+ bytes += (await fs20.promises.lstat(full)).size;
7143
+ } catch {
7144
+ }
7145
+ }
7146
+ }
7147
+ return bytes;
7148
+ }
7149
+ var RunSampler = class {
7150
+ constructor(options = {}) {
7151
+ this.timer = null;
7152
+ this.running = null;
7153
+ this.samples = 0;
7154
+ this.startedAt = 0;
7155
+ this.tracked = /* @__PURE__ */ new Map();
7156
+ this.treeMeasured = false;
7157
+ this.waitMeasured = false;
7158
+ this.cpuStart = null;
7159
+ this.cpuPrevious = null;
7160
+ this.series = [];
7161
+ this.cpuPoints = [];
7162
+ this.memoryPoints = [];
7163
+ /** Each worker process's tree, by the worker's process key. */
7164
+ this.workerTrees = /* @__PURE__ */ new Map();
7165
+ /** When the workers' CPU was last read, for the cores they used since. */
7166
+ this.workersReadAt = null;
7167
+ this.pressureStart = { cpu: null, memory: null };
7168
+ this.containerStart = null;
7169
+ this.memoryKind = null;
7170
+ this.peakBytes = null;
7171
+ this.peakAtMs = null;
7172
+ this.largest = null;
7173
+ this.rssFallbackPids = /* @__PURE__ */ new Set();
7174
+ this.lowestAvailableKb = null;
7175
+ this.peakInUse = null;
7176
+ this.peakInUseIsLowerBound = false;
7177
+ this.lowestFree = null;
7178
+ this.rootPid = options.rootPid ?? process.pid;
7179
+ this.platform = options.platform ?? process.platform;
7180
+ this.proc = new ProcFs(options.procRoot);
7181
+ this.cgroup = new CgroupFs(options.procRoot, options.cgroupRoot);
7182
+ this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path22.resolve(dir)))];
7183
+ this.tmpDir = options.tmpDir ?? os8.tmpdir();
7184
+ this.intervalMs = options.intervalMs ?? 1e3;
7185
+ this.heavyEvery = Math.max(1, options.heavyEvery ?? 5);
7186
+ this.windowsTreeEvery = Math.max(1, options.windowsTreeEvery ?? 3);
7187
+ this.now = options.now ?? Date.now;
7188
+ this.runPs = options.runPs ?? defaultPs;
7189
+ this.runWindows = options.runWindows ?? defaultWindowsQuery;
7190
+ this.linux = this.platform === "linux" && this.proc.available();
7191
+ }
7192
+ /** Take the first sample and sample on a timer that never keeps the process alive. */
7193
+ start() {
7194
+ if (this.timer) return;
7195
+ this.startedAt = this.now();
7196
+ this.cpuStart = this.cpuTicks();
7197
+ this.cpuPrevious = this.cpuStart;
7198
+ if (this.linux) {
7199
+ this.pressureStart = { cpu: this.proc.pressureUs("cpu"), memory: this.proc.pressureUs("memory") };
7200
+ this.containerStart = this.cgroup.read();
7201
+ }
7202
+ this.schedule();
7203
+ this.timer = setInterval(() => this.schedule(), this.intervalMs);
7204
+ this.timer.unref?.();
7205
+ }
7206
+ /** Take the last sample and return the run's profile. Never throws. */
7207
+ async stop() {
7208
+ if (this.timer) clearInterval(this.timer);
7209
+ this.timer = null;
7210
+ try {
7211
+ await this.running;
7212
+ await this.sample(true, true);
7213
+ } catch {
7214
+ }
7215
+ return this.profile(await this.leftoverBytes());
7216
+ }
7217
+ /** The series read so far, each point at the time it was read. */
7218
+ timedSeries() {
7219
+ return {
7220
+ startedAt: this.startedAt,
7221
+ cpuPct: [...this.cpuPoints],
7222
+ memoryBytes: [...this.memoryPoints],
7223
+ workers: [...this.workerTrees.values()].map((tree) => ({
7224
+ pid: tree.pid,
7225
+ cpuCores: [...tree.cpuCores],
7226
+ memoryBytes: [...tree.memoryBytes]
7227
+ }))
7228
+ };
7229
+ }
7230
+ schedule() {
7231
+ if (this.running) return;
7232
+ const heavy = this.samples % this.heavyEvery === 0;
7233
+ this.running = this.sample(heavy).catch(() => void 0).finally(() => {
7234
+ this.running = null;
7235
+ });
7236
+ }
7237
+ cpuTicks() {
7238
+ return this.linux ? this.proc.cpuTicks() : nodeCpuTicks();
7239
+ }
7240
+ async sample(heavy, forceTree = false) {
7241
+ const treeDue = forceTree || this.samples % (this.heavyEvery * this.windowsTreeEvery) === 0;
7242
+ this.samples++;
7243
+ const at = this.now() - this.startedAt;
7244
+ const ticks = this.cpuTicks();
7245
+ if (ticks && this.cpuPrevious) {
7246
+ const busy = pct(ticks.busy - this.cpuPrevious.busy, ticks.total - this.cpuPrevious.total);
7247
+ if (busy !== null && this.series.length < SERIES_CAP) this.series.push(busy);
7248
+ if (busy !== null) this.cpuPoints = pushTimed(this.cpuPoints, [at, busy], mean);
7249
+ }
7250
+ if (ticks) this.cpuPrevious = ticks;
7251
+ const availableKb = this.linux ? this.proc.memAvailableKb() : Math.round(os8.freemem() / 1024);
7252
+ if (availableKb !== null) this.lowestAvailableKb = Math.min(this.lowestAvailableKb ?? Infinity, availableKb);
7253
+ if (this.linux) await this.sampleLinuxTree(heavy, at);
7254
+ else if (this.platform === "darwin" && heavy) await this.samplePsTree(at);
7255
+ else if (this.platform === "win32" && treeDue) await this.sampleWindowsTree(at);
7256
+ if (heavy) await this.sampleDisk();
7257
+ }
7258
+ track(key, role, cpuMs, waitMs) {
7259
+ const known = this.tracked.get(key);
7260
+ if (known) {
7261
+ known.lastCpuMs = cpuMs;
7262
+ known.lastWaitMs = waitMs;
7263
+ if (role) known.role = role;
7264
+ return;
7265
+ }
7266
+ const first = this.samples === 1;
7267
+ this.tracked.set(key, {
7268
+ role,
7269
+ firstCpuMs: first ? cpuMs : 0,
7270
+ firstWaitMs: first ? waitMs : waitMs === null ? null : 0,
7271
+ lastCpuMs: cpuMs,
7272
+ lastWaitMs: waitMs
7273
+ });
7274
+ }
7275
+ /**
7276
+ * The worker each process of the tree belongs to: a worker process itself,
7277
+ * or the worker it descends from (its browsers and their children). The
7278
+ * runner and the web server belong to none. The tree lists parents first.
7279
+ */
7280
+ workersOf(tree, roles, keyOf2) {
7281
+ const out = /* @__PURE__ */ new Map();
7282
+ for (const proc of tree) {
7283
+ if (roles.get(proc.pid) === "worker") out.set(proc.pid, { key: keyOf2(proc), pid: proc.pid });
7284
+ else {
7285
+ const parent = out.get(proc.ppid);
7286
+ if (parent) out.set(proc.pid, parent);
7287
+ }
7288
+ }
7289
+ return out;
7290
+ }
7291
+ /** The CPU a process used since the previous sample: all of it for one born since, none at the first sample. */
7292
+ cpuSince(key, cpuMs) {
7293
+ const known = this.tracked.get(key);
7294
+ if (known) return Math.max(0, cpuMs - known.lastCpuMs);
7295
+ return this.samples === 1 ? 0 : cpuMs;
7296
+ }
7297
+ /**
7298
+ * Add a reading to each worker's tree: the cores its processes used since
7299
+ * the previous reading, and on heavy samples their memory.
7300
+ */
7301
+ noteWorkers(workers, cpuMsByWorker, bytesByWorker, at) {
7302
+ const elapsed = this.workersReadAt === null ? 0 : at - this.workersReadAt;
7303
+ for (const { key, pid } of new Map([...workers.values()].map((w) => [w.key, w])).values()) {
7304
+ let tree = this.workerTrees.get(key);
7305
+ if (!tree) {
7306
+ tree = { pid, cpuCores: [], memoryBytes: [] };
7307
+ this.workerTrees.set(key, tree);
7308
+ }
7309
+ if (elapsed > 0) {
7310
+ const cores = Math.round((cpuMsByWorker.get(key) ?? 0) / elapsed * 100) / 100;
7311
+ tree.cpuCores = pushTimed(tree.cpuCores, [at, cores], mean2);
7312
+ }
7313
+ const bytes = bytesByWorker?.get(key);
7314
+ if (bytes !== void 0) tree.memoryBytes = pushTimed(tree.memoryBytes, [at, bytes], Math.max);
7315
+ }
7316
+ this.workersReadAt = at;
7317
+ }
7318
+ /** The role of each process of the tree, a child of the runner being the web server unless it says otherwise. */
7319
+ resolveRoles(tree, explicit) {
7320
+ const roles = /* @__PURE__ */ new Map([[this.rootPid, "runner"]]);
7321
+ for (const proc of tree) {
7322
+ const parent = roles.get(proc.ppid);
7323
+ const inherited = parent === "runner" || parent === "webServer" ? "webServer" : "other";
7324
+ roles.set(proc.pid, explicit(proc) ?? inherited);
7325
+ }
7326
+ return roles;
7327
+ }
7328
+ async sampleLinuxTree(heavy, at) {
7329
+ const table = this.proc.table();
7330
+ const root = table.get(this.rootPid);
7331
+ if (!root) return;
7332
+ this.treeMeasured = true;
7333
+ const tree = [root, ...descendantsOf(this.rootPid, table.values())];
7334
+ const roles = this.resolveRoles(tree.slice(1), (proc) => {
7335
+ const key = `${proc.pid}:${proc.start}`;
7336
+ const known = this.tracked.get(key)?.role;
7337
+ return known && !heavy ? known : roleOf(proc.comm, this.proc.cmdline(proc.pid));
7338
+ });
7339
+ const workers = this.workersOf(tree, roles, (proc) => `${proc.pid}:${proc.start}`);
7340
+ const cpuMsByWorker = /* @__PURE__ */ new Map();
7341
+ for (const proc of tree) {
7342
+ const key = `${proc.pid}:${proc.start}`;
7343
+ const worker = workers.get(proc.pid);
7344
+ if (worker) {
7345
+ cpuMsByWorker.set(worker.key, (cpuMsByWorker.get(worker.key) ?? 0) + this.cpuSince(key, proc.cpuMs));
7346
+ }
7347
+ const waitMs = this.proc.runWaitMs(proc.pid);
7348
+ if (waitMs !== null) this.waitMeasured = true;
7349
+ this.track(key, roles.get(proc.pid) ?? "other", proc.cpuMs, waitMs);
7350
+ }
7351
+ if (!heavy) {
7352
+ this.noteWorkers(workers, cpuMsByWorker, null, at);
7353
+ return;
7354
+ }
7355
+ let total = 0;
7356
+ let largest = null;
7357
+ const memory = await Promise.all(
7358
+ tree.map(async (proc) => {
7359
+ const pss = await this.proc.pssKb(proc.pid);
7360
+ if (pss !== null) return { proc, kb: pss, fallback: false };
7361
+ return { proc, kb: this.proc.statusKb(proc.pid, "VmRSS"), fallback: true };
7362
+ })
7363
+ );
7364
+ const bytesByWorker = /* @__PURE__ */ new Map();
7365
+ for (const { proc, kb, fallback } of memory) {
7366
+ if (kb === null) continue;
7367
+ if (fallback) this.rssFallbackPids.add(`${proc.pid}:${proc.start}`);
7368
+ total += kb * 1024;
7369
+ if (!largest || kb * 1024 > largest.bytes) largest = { role: roles.get(proc.pid) ?? "other", bytes: kb * 1024 };
7370
+ const worker = workers.get(proc.pid);
7371
+ if (worker) bytesByWorker.set(worker.key, (bytesByWorker.get(worker.key) ?? 0) + kb * 1024);
7372
+ }
7373
+ this.memoryKind = memory.some((m) => m.kb !== null && !m.fallback) ? "pss" : memory.length ? "rss" : null;
7374
+ this.notePeak(total, largest, at);
7375
+ this.noteWorkers(workers, cpuMsByWorker, bytesByWorker, at);
7376
+ }
7377
+ async samplePsTree(at) {
7378
+ let list;
7379
+ try {
7380
+ list = parsePsOutput(await this.runPs());
7381
+ } catch {
7382
+ return;
7383
+ }
7384
+ this.noteTree(list, (proc) => roleOf(path22.basename(proc.command.split(" ")[0] ?? ""), proc.command), at);
7385
+ }
7386
+ async sampleWindowsTree(at) {
7387
+ let list;
7388
+ try {
7389
+ list = parseWindowsProcesses(await this.runWindows());
7390
+ } catch {
7391
+ return;
7392
+ }
7393
+ this.noteTree(list, (proc) => roleOf(proc.name, proc.commandLine), at);
7394
+ }
7395
+ /** One reading of the run's tree: each process's role, CPU time and memory, and each worker's share of both. */
7396
+ noteTree(list, role, at) {
7397
+ const root = list.find((proc) => proc.pid === this.rootPid);
7398
+ if (!root) return;
7399
+ this.treeMeasured = true;
7400
+ const tree = [root, ...descendantsOf(this.rootPid, list)];
7401
+ const roles = this.resolveRoles(tree.slice(1), role);
7402
+ let total = 0;
7403
+ let largest = null;
7404
+ const workers = this.workersOf(tree, roles, (proc) => String(proc.pid));
7405
+ const cpuMsByWorker = /* @__PURE__ */ new Map();
7406
+ const bytesByWorker = /* @__PURE__ */ new Map();
7407
+ for (const proc of tree) {
7408
+ const procRole = roles.get(proc.pid) ?? "other";
7409
+ const worker = workers.get(proc.pid);
7410
+ if (worker) {
7411
+ const cpuMs = this.cpuSince(String(proc.pid), proc.cpuMs);
7412
+ cpuMsByWorker.set(worker.key, (cpuMsByWorker.get(worker.key) ?? 0) + cpuMs);
7413
+ bytesByWorker.set(worker.key, (bytesByWorker.get(worker.key) ?? 0) + proc.rssKb * 1024);
7414
+ }
7415
+ this.track(String(proc.pid), procRole, proc.cpuMs, null);
7416
+ total += proc.rssKb * 1024;
7417
+ if (!largest || proc.rssKb * 1024 > largest.bytes) largest = { role: procRole, bytes: proc.rssKb * 1024 };
7418
+ }
7419
+ this.memoryKind = "rss";
7420
+ this.notePeak(total, largest, at);
7421
+ this.noteWorkers(workers, cpuMsByWorker, bytesByWorker, at);
7422
+ }
7423
+ notePeak(total, largest, at) {
7424
+ this.memoryPoints = pushTimed(this.memoryPoints, [at, total], Math.max);
7425
+ if (this.peakBytes === null || total > this.peakBytes) {
7426
+ this.peakBytes = total;
7427
+ this.peakAtMs = this.now() - this.startedAt;
7428
+ }
7429
+ if (largest && (!this.largest || largest.bytes > this.largest.bytes)) this.largest = largest;
7430
+ }
7431
+ async sampleDisk() {
7432
+ for (const dir of [...this.outputDirs, this.tmpDir]) {
7433
+ const free = freeBytes(dir);
7434
+ if (free !== null) this.lowestFree = Math.min(this.lowestFree ?? Infinity, free);
7435
+ }
7436
+ const budget = { files: WALK_FILE_CAP };
7437
+ let bytes = 0;
7438
+ for (const dir of this.outputDirs) bytes += await dirBytes(dir, budget);
7439
+ for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs >= this.startedAt - 1e3)) {
7440
+ bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
7441
+ }
7442
+ if (budget.files <= 0) this.peakInUseIsLowerBound = true;
7443
+ this.peakInUse = Math.max(this.peakInUse ?? 0, bytes);
7444
+ }
7445
+ /** Playwright's folders in the temp directory whose modification time passes `keep`. */
7446
+ async playwrightTempEntries(keep) {
7447
+ let names;
7448
+ try {
7449
+ names = await fs20.promises.readdir(this.tmpDir);
7450
+ } catch {
7451
+ return [];
7452
+ }
7453
+ const out = [];
7454
+ for (const name of names) {
7455
+ if (!PLAYWRIGHT_TEMP.test(name)) continue;
7456
+ try {
7457
+ if (keep((await fs20.promises.stat(path22.join(this.tmpDir, name))).mtimeMs)) out.push(name);
7458
+ } catch {
7459
+ }
7460
+ }
7461
+ return out;
7462
+ }
7463
+ async leftoverBytes() {
7464
+ try {
7465
+ const budget = { files: WALK_FILE_CAP };
7466
+ let bytes = 0;
7467
+ for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs < this.startedAt - 1e3)) {
7468
+ bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
7469
+ }
7470
+ return bytes;
7471
+ } catch {
7472
+ return null;
7473
+ }
7474
+ }
7475
+ profile(leftoverBytes) {
7476
+ const wallMs = Math.max(0, this.now() - this.startedAt);
7477
+ const notMeasured = [];
7478
+ let byRole = null;
7479
+ if (this.treeMeasured) {
7480
+ byRole = {};
7481
+ for (const entry of this.tracked.values()) {
7482
+ const role = entry.role ?? "other";
7483
+ const usage = byRole[role] ??= { cpuMs: 0, runWaitMs: this.waitMeasured ? 0 : null, processes: 0 };
7484
+ usage.cpuMs += Math.max(0, entry.lastCpuMs - entry.firstCpuMs);
7485
+ if (usage.runWaitMs !== null && entry.lastWaitMs !== null && entry.firstWaitMs !== null) {
7486
+ usage.runWaitMs += Math.max(0, entry.lastWaitMs - entry.firstWaitMs);
7487
+ }
7488
+ usage.processes++;
7489
+ }
7490
+ } else {
7491
+ notMeasured.push("CPU and memory of the run\u2019s processes");
7492
+ }
7493
+ if (this.treeMeasured && !this.waitMeasured) notMeasured.push("time waiting for a CPU");
7494
+ const end = this.cpuTicks();
7495
+ const start = this.cpuStart;
7496
+ const total = end && start ? end.total - start.total : 0;
7497
+ const machineCpu = {
7498
+ busyPct: end && start ? pct(end.busy - start.busy, total) : null,
7499
+ iowaitPct: this.linux && end && start ? pct(end.iowait - start.iowait, total) : null,
7500
+ stealPct: this.linux && end && start ? pct(end.steal - start.steal, total) : null
7501
+ };
7502
+ if (!this.linux) notMeasured.push("steal");
7503
+ const pressure = (resource, from) => {
7504
+ if (!this.linux || from === null) return null;
7505
+ const to = this.proc.pressureUs(resource);
7506
+ return to === null || wallMs === 0 ? null : Math.min(100, round12((to - from) / 1e3 / wallMs * 100));
7507
+ };
7508
+ const cpuPressure = pressure("cpu", this.pressureStart.cpu);
7509
+ const memoryPressure = pressure("memory", this.pressureStart.memory);
7510
+ if (cpuPressure === null) notMeasured.push("CPU pressure");
7511
+ const container = this.linux ? this.cgroup.read() : null;
7512
+ const startContainer = this.containerStart;
7513
+ const totalMemory = os8.totalmem();
7514
+ const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
7515
+ const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
7516
+ const raisedPeak = container?.memoryPeakBytes != null && startContainer?.memoryPeakBytes != null && container.memoryPeakBytes > startContainer.memoryPeakBytes ? container.memoryPeakBytes : null;
7517
+ const delta = (to, from) => to != null && from != null ? Math.max(0, to - from) : null;
7518
+ return {
7519
+ platform: this.platform,
7520
+ wallMs,
7521
+ machine: {
7522
+ cores: typeof os8.availableParallelism === "function" ? os8.availableParallelism() : os8.cpus().length,
7523
+ memoryBytes: totalMemory,
7524
+ memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null,
7525
+ cpuQuotaCores: container?.cpuQuotaCores ?? null
7526
+ },
7527
+ cpu: {
7528
+ ...machineCpu,
7529
+ pressurePct: cpuPressure,
7530
+ series: this.series,
7531
+ byRole,
7532
+ throttledMs: delta(container?.throttledMs, startContainer?.throttledMs)
7533
+ },
7534
+ memory: {
7535
+ kind: this.memoryKind,
7536
+ peakBytes: this.peakBytes,
7537
+ peakAtMs: this.peakAtMs,
7538
+ largest: this.largest,
7539
+ rssFallbacks: this.memoryKind === "pss" ? this.rssFallbackPids.size : 0,
7540
+ pressurePct: memoryPressure,
7541
+ lowestAvailableBytes: this.lowestAvailableKb === null ? null : this.lowestAvailableKb * 1024,
7542
+ containerPeakBytes: raisedPeak,
7543
+ oomKills: delta(container?.oomKills, startContainer?.oomKills)
7544
+ },
7545
+ disk: {
7546
+ peakInUseBytes: this.peakInUse,
7547
+ peakInUseIsLowerBound: this.peakInUseIsLowerBound,
7548
+ lowestFreeBytes: this.lowestFree,
7549
+ leftoverBytes
7550
+ },
7551
+ notMeasured
7552
+ };
7553
+ }
7554
+ };
7555
+
7556
+ // src/internal/collect/machine-panel.ts
7557
+ function workerHealthOf(censuses) {
7558
+ const measured = censuses.filter((census) => census.test && census.metrics);
7559
+ if (measured.length === 0) return null;
7560
+ let utilization = 0;
7561
+ let switches = 0;
7562
+ let worstP99 = 0;
7563
+ for (const census of measured) {
7564
+ const worker = census.metrics.worker;
7565
+ utilization += worker.loopUtilization;
7566
+ switches += worker.involuntarySwitches;
7567
+ worstP99 = Math.max(worstP99, worker.loopDelayP99Ms);
7568
+ }
7569
+ return {
7570
+ tests: measured.length,
7571
+ loopUtilization: utilization / measured.length,
7572
+ loopDelayP99Ms: worstP99,
7573
+ involuntarySwitchesPerTest: switches / measured.length
7574
+ };
7575
+ }
7576
+ function artifactKind(name, contentType) {
7577
+ if (name === "trace") return "trace";
7578
+ if (name === "video" || contentType.startsWith("video/")) return "video";
7579
+ if (name === "screenshot" || contentType.startsWith("image/")) return "screenshot";
7580
+ return "other";
7581
+ }
7582
+ function emptyArtifacts() {
7583
+ return { trace: 0, video: 0, screenshot: 0, other: 0 };
7584
+ }
7585
+ var ROLE_LABEL = {
7586
+ renderer: "renderers",
7587
+ browser: "browsers",
7588
+ worker: "workers",
7589
+ gpu: "GPU",
7590
+ utility: "browser utilities",
7591
+ ffmpeg: "ffmpeg",
7592
+ webServer: "web server",
7593
+ runner: "runner",
7594
+ other: "other"
7595
+ };
7596
+ var ROLE_SINGULAR = {
7597
+ renderer: "renderer",
7598
+ browser: "browser",
7599
+ worker: "worker",
7600
+ gpu: "GPU",
7601
+ utility: "browser utility",
7602
+ ffmpeg: "ffmpeg",
7603
+ webServer: "web server",
7604
+ runner: "runner",
7605
+ other: "other"
7606
+ };
7607
+ var ARTIFACT_LABEL = {
7608
+ trace: "traces",
7609
+ video: "videos",
7610
+ screenshot: "screenshots",
7611
+ other: "other"
7612
+ };
7613
+ var BARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
7614
+ function formatBytes(bytes) {
7615
+ if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
7616
+ if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MB`;
7617
+ return `${Math.max(0, Math.round(bytes / 1024))} kB`;
7618
+ }
7619
+ function sparkline(series, width = 24) {
7620
+ if (series.length < 3) return "";
7621
+ const buckets = Math.min(width, series.length);
7622
+ let out = "";
7623
+ for (let i = 0; i < buckets; i++) {
7624
+ const slice = series.slice(
7625
+ Math.floor(i * series.length / buckets),
7626
+ Math.floor((i + 1) * series.length / buckets)
7627
+ );
7628
+ const mean3 = slice.reduce((sum, v) => sum + v, 0) / Math.max(1, slice.length);
7629
+ out += BARS[Math.min(BARS.length - 1, Math.max(0, Math.floor(mean3 / 100 * BARS.length)))];
7630
+ }
7631
+ return out;
7632
+ }
7633
+ function clock(ms) {
7634
+ const seconds = Math.round(ms / 1e3);
7635
+ return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
7636
+ }
7637
+ function cpuLine(profile) {
7638
+ const parts = [];
7639
+ const spark = sparkline(profile.cpu.series);
7640
+ if (profile.cpu.busyPct !== null) parts.push(`${spark ? `${spark} ` : ""}${Math.round(profile.cpu.busyPct)}% busy`);
7641
+ if (profile.cpu.pressurePct !== null) {
7642
+ parts.push(`a task waited for a CPU ${Math.round(profile.cpu.pressurePct)}% of the time`);
7643
+ }
7644
+ const roles = Object.entries(profile.cpu.byRole ?? {});
7645
+ const total = roles.reduce((sum, [, usage]) => sum + (usage?.cpuMs ?? 0), 0);
7646
+ if (total > 0) {
7647
+ const top = roles.filter(([, usage]) => (usage?.cpuMs ?? 0) >= total * 0.02).sort((a, b) => (b[1]?.cpuMs ?? 0) - (a[1]?.cpuMs ?? 0)).slice(0, 5).map(([role, usage]) => `${ROLE_LABEL[role]} ${formatHeld(usage.cpuMs)}`);
7648
+ parts.push(`${formatHeld(total)} of CPU: ${top.join(" \xB7 ")}`);
7649
+ const waiting = roles.filter(([, usage]) => (usage?.runWaitMs ?? 0) >= 1e3).sort((a, b) => (b[1]?.runWaitMs ?? 0) - (a[1]?.runWaitMs ?? 0))[0];
7650
+ if (waiting) parts.push(`${ROLE_LABEL[waiting[0]]} waited ${formatHeld(waiting[1].runWaitMs)} for a CPU`);
7651
+ }
7652
+ if (profile.cpu.throttledMs)
7653
+ parts.push(`the container's CPU quota held it back ${formatHeld(profile.cpu.throttledMs)}`);
7654
+ return parts.length ? ` CPU ${parts.join(" \xB7 ")}` : null;
7655
+ }
7656
+ function memoryLine(profile) {
7657
+ const { memory, machine } = profile;
7658
+ const parts = [];
7659
+ if (memory.kind && memory.peakBytes !== null) {
7660
+ const fallbacks = memory.rssFallbacks ? `, ${memory.rssFallbacks} by RSS` : "";
7661
+ const at = memory.peakAtMs !== null ? ` at ${clock(memory.peakAtMs)}` : "";
7662
+ parts.push(`peak ${formatBytes(memory.peakBytes)} (${memory.kind.toUpperCase()}${fallbacks})${at}`);
7663
+ const limit = machine.memoryLimitBytes;
7664
+ const whole = limit ?? machine.memoryBytes;
7665
+ if (whole > 0)
7666
+ parts.push(`${Math.round(memory.peakBytes / whole * 100)}% of the ${limit ? "limit" : "machine's memory"}`);
7667
+ if (memory.largest)
7668
+ parts.push(`largest process: ${ROLE_SINGULAR[memory.largest.role]} ${formatBytes(memory.largest.bytes)}`);
7669
+ }
7670
+ if (memory.containerPeakBytes !== null)
7671
+ parts.push(`the container peaked at ${formatBytes(memory.containerPeakBytes)}`);
7672
+ if (memory.oomKills) parts.push(`${memory.oomKills} process${memory.oomKills === 1 ? "" : "es"} killed for memory`);
7673
+ if (memory.pressurePct !== null) {
7674
+ parts.push(
7675
+ memory.pressurePct >= 0.1 ? `a task waited for memory ${memory.pressurePct}% of the time` : "no memory pressure"
7676
+ );
7677
+ }
7678
+ if (memory.lowestAvailableBytes !== null && memory.lowestAvailableBytes < machine.memoryBytes * 0.1) {
7679
+ parts.push(`${formatBytes(memory.lowestAvailableBytes)} available at the low point`);
7680
+ }
7681
+ return parts.length ? ` Memory ${parts.join(" \xB7 ")}` : null;
7682
+ }
7683
+ function diskLine(profile, artifacts) {
7684
+ const parts = [];
7685
+ if (artifacts) {
7686
+ const kinds = Object.entries(artifacts).filter(([, bytes]) => bytes > 0);
7687
+ const total = kinds.reduce((sum, [, bytes]) => sum + bytes, 0);
7688
+ parts.push(
7689
+ total > 0 ? `${formatBytes(total)} of artifacts (${kinds.sort((a, b) => b[1] - a[1]).map(([kind, bytes]) => `${ARTIFACT_LABEL[kind]} ${formatBytes(bytes)}`).join(" \xB7 ")})` : "no artifacts"
7690
+ );
7691
+ }
7692
+ const { disk } = profile;
7693
+ if (disk.peakInUseBytes !== null && disk.peakInUseBytes > 0) {
7694
+ parts.push(
7695
+ `${disk.peakInUseIsLowerBound ? "at least " : ""}${formatBytes(disk.peakInUseBytes)} in use at the peak`
7696
+ );
7697
+ }
7698
+ if (disk.lowestFreeBytes !== null) parts.push(`${formatBytes(disk.lowestFreeBytes)} free at the low point`);
7699
+ if (disk.leftoverBytes !== null && disk.leftoverBytes >= 10 * 1024 ** 2) {
7700
+ parts.push(`${formatBytes(disk.leftoverBytes)} left in the temp folder by earlier runs`);
7701
+ }
7702
+ return parts.length ? ` Disk ${parts.join(" \xB7 ")}` : null;
7703
+ }
7704
+ function workersLine(workers) {
7705
+ if (!workers || workers.tests === 0) return null;
7706
+ return ` Workers event loop busy ${Math.round(workers.loopUtilization * 100)}% \xB7 p99 delay ${Math.round(
7707
+ workers.loopDelayP99Ms
7708
+ )} ms at worst \xB7 ${Math.round(workers.involuntarySwitchesPerTest).toLocaleString("en-US")} involuntary context switches per test`;
7709
+ }
7710
+ function formatMachinePanel(profile, extras) {
7711
+ const { machine, cpu } = profile;
7712
+ const head = [
7713
+ `${machine.cores} cores${machine.cpuQuotaCores ? ` (a quota of ${machine.cpuQuotaCores})` : ""}`,
7714
+ formatBytes(machine.memoryBytes)
7715
+ ];
7716
+ if (machine.memoryLimitBytes) head.push(`container limit ${formatBytes(machine.memoryLimitBytes)}`);
7717
+ if (cpu.stealPct !== null && cpu.stealPct >= 1) head.push(`steal ${cpu.stealPct}%`);
7718
+ const lines = [
7719
+ `Machine: ${head.join(" \xB7 ")}`,
7720
+ cpuLine(profile),
7721
+ memoryLine(profile),
7722
+ diskLine(profile, extras.artifacts),
7723
+ workersLine(extras.workers)
7724
+ ].filter((line) => line !== null);
7725
+ if (profile.notMeasured.length) lines.push(` Not measured here: ${profile.notMeasured.join(", ")}`);
7726
+ return lines;
7727
+ }
7728
+
7729
+ // src/internal/collect/resource-wire.ts
7730
+ var REPORT_FINDINGS_CAP = 100;
7731
+ var REPORT_SERIES_CAP = 240;
7732
+ var REPORT_WORKER_POINTS_CAP = 200;
7733
+ var TIMELINE_SERIES_CAP = 600;
7734
+ var TIMELINE_WORKER_POINTS_CAP = 300;
7735
+ var TIMELINE_WORKERS_BUDGET = 6e3;
7736
+ function downsample(series, cap) {
7737
+ if (series.length <= cap) return series;
7738
+ const out = [];
7739
+ for (let i = 0; i < cap; i++) {
7740
+ const slice = series.slice(Math.floor(i * series.length / cap), Math.floor((i + 1) * series.length / cap));
7741
+ out.push(Math.round(slice.reduce((sum, v) => sum + v, 0) / Math.max(1, slice.length) * 10) / 10);
7742
+ }
7743
+ return out;
7744
+ }
7745
+ function downsamplePoints(points, cap, merge) {
7746
+ if (points.length <= cap) return points;
7747
+ const out = [];
7748
+ for (let i = 0; i < cap; i++) {
7749
+ const slice = points.slice(Math.floor(i * points.length / cap), Math.floor((i + 1) * points.length / cap));
7750
+ if (slice.length === 0) continue;
7751
+ const values = slice.map((point) => point[1]);
7752
+ const value = merge === "max" ? Math.max(...values) : Math.round(values.reduce((sum, v) => sum + v, 0) / values.length * 10) / 10;
7753
+ out.push([slice[0][0], value]);
7754
+ }
7755
+ return out;
7756
+ }
7757
+ function nonZero(bytes) {
7758
+ const out = Object.fromEntries(Object.entries(bytes).filter(([, n]) => (n ?? 0) > 0));
7759
+ return Object.keys(out).length > 0 ? out : null;
7760
+ }
7761
+ function executionResources(census, artifacts) {
7762
+ const metrics = census.metrics;
7763
+ if (!metrics) return null;
7764
+ return {
7765
+ workerCpuMs: metrics.worker.cpuMs,
7766
+ roles: metrics.roles ?? null,
7767
+ loopUtilization: metrics.worker.loopUtilization,
7768
+ loopDelayP99Ms: metrics.worker.loopDelayP99Ms,
7769
+ involuntarySwitches: metrics.worker.involuntarySwitches,
7770
+ heapUsedMb: metrics.worker.heapUsedMb,
7771
+ openAtStart: census.openAtStart ?? null,
7772
+ leftOpen: census.leftOpen ?? null,
7773
+ artifactBytes: nonZero(artifacts)
7774
+ };
7775
+ }
7776
+ function openPagesByWorker(censuses) {
7777
+ const byWorker = /* @__PURE__ */ new Map();
7778
+ for (const census of censuses) {
7779
+ const list = byWorker.get(census.worker) ?? [];
7780
+ list.push(census);
7781
+ byWorker.set(census.worker, list);
7782
+ }
7783
+ const out = [];
7784
+ for (const [worker, list] of [...byWorker.entries()].sort((a, b) => a[0] - b[0])) {
7785
+ list.sort((a, b) => a.at - b.at);
7786
+ const kinds = /* @__PURE__ */ new Map();
7787
+ const points = [];
7788
+ for (const census of list) {
7789
+ for (const birth of census.born) kinds.set(birth.id, birth.kind);
7790
+ if (census.test) points.push(census.open.filter((open) => kinds.get(open.id) === "page").length);
7791
+ }
7792
+ if (points.length > 0) out.push({ worker, openPages: downsample(points, REPORT_WORKER_POINTS_CAP) });
7793
+ }
7794
+ return out;
7795
+ }
7796
+ function pagesOverTime(censuses) {
7797
+ const byWorker = /* @__PURE__ */ new Map();
7798
+ for (const census of censuses) {
7799
+ const list = byWorker.get(census.worker) ?? [];
7800
+ list.push(census);
7801
+ byWorker.set(census.worker, list);
7802
+ }
7803
+ const out = [];
7804
+ for (const [worker, list] of [...byWorker.entries()].sort((a, b) => a[0] - b[0])) {
7805
+ const pages = /* @__PURE__ */ new Set();
7806
+ const changes = [];
7807
+ let lastAt = -Infinity;
7808
+ for (const census of list) {
7809
+ lastAt = Math.max(lastAt, census.at);
7810
+ for (const birth of census.born) {
7811
+ if (birth.kind !== "page") continue;
7812
+ pages.add(birth.id);
7813
+ changes.push([birth.at, 1]);
7814
+ }
7815
+ }
7816
+ for (const census of list) {
7817
+ for (const close of census.closed) if (pages.has(close.id)) changes.push([close.at, -1]);
7818
+ }
7819
+ if (changes.length === 0) continue;
7820
+ changes.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
7821
+ const events = [];
7822
+ let open = 0;
7823
+ for (const [at, delta] of changes) {
7824
+ open = Math.max(0, open + delta);
7825
+ const last = events[events.length - 1];
7826
+ if (last && last[0] === at) last[1] = open;
7827
+ else events.push([at, open]);
7828
+ }
7829
+ if (open > 0 && lastAt >= events[events.length - 1][0]) events.push([lastAt, 0]);
7830
+ out.push({ worker, events });
7831
+ }
7832
+ return out;
7833
+ }
7834
+ function workerTreesByIndex(samples, censuses) {
7835
+ const byPid = /* @__PURE__ */ new Map();
7836
+ for (const census of censuses) {
7837
+ const list = byPid.get(census.pid) ?? [];
7838
+ list.push({ worker: census.worker, at: census.at });
7839
+ byPid.set(census.pid, list);
7840
+ }
7841
+ const out = /* @__PURE__ */ new Map();
7842
+ for (const tree of samples.workers) {
7843
+ const candidates = byPid.get(tree.pid);
7844
+ const times = [...tree.cpuCores, ...tree.memoryBytes].map(([at]) => samples.startedAt + at);
7845
+ if (!candidates || times.length === 0) continue;
7846
+ const from = Math.min(...times);
7847
+ const to = Math.max(...times);
7848
+ const distance = (at) => at < from ? from - at : at > to ? at - to : 0;
7849
+ const nearest = candidates.reduce((best, c) => distance(c.at) < distance(best.at) ? c : best);
7850
+ if (!out.has(nearest.worker)) out.set(nearest.worker, { worker: nearest.worker, ...tree });
7851
+ }
7852
+ return [...out.values()].sort((a, b) => a.worker - b.worker);
7853
+ }
7854
+ function resourceTimeline(samples, censuses) {
7855
+ const workers = pagesOverTime(censuses);
7856
+ const hasSamples = samples !== null && (samples.cpuPct.length > 0 || samples.memoryBytes.length > 0);
7857
+ if (!hasSamples && workers.length === 0) return null;
7858
+ let startedAt = hasSamples ? samples.startedAt : Infinity;
7859
+ for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
7860
+ const shift = hasSamples ? samples.startedAt - startedAt : 0;
7861
+ const relative13 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
7862
+ const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
7863
+ const perWorker = Math.max(
7864
+ 60,
7865
+ Math.min(TIMELINE_WORKER_POINTS_CAP, Math.floor(TIMELINE_WORKERS_BUDGET / Math.max(1, 2 * trees.length)))
7866
+ );
7867
+ return {
7868
+ startedAt,
7869
+ cpuPct: hasSamples ? downsamplePoints(relative13(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
7870
+ memoryBytes: hasSamples ? downsamplePoints(relative13(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
7871
+ pages: workers.map(({ worker, events }) => {
7872
+ const points = relative13(events, -startedAt);
7873
+ const last = points.pop();
7874
+ return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
7875
+ }),
7876
+ workers: trees.map((tree) => ({
7877
+ worker: tree.worker,
7878
+ cpuCores: downsamplePoints(relative13(tree.cpuCores, shift), perWorker, "mean"),
7879
+ memoryBytes: downsamplePoints(relative13(tree.memoryBytes, shift), perWorker, "max")
7880
+ }))
7881
+ };
7882
+ }
7883
+ function resourceReportWire(input) {
7884
+ const { report, profile } = input;
7885
+ return {
7886
+ v: 1,
7887
+ shardIndex: input.shardIndex,
7888
+ findings: (report?.findings ?? []).slice(0, REPORT_FINDINGS_CAP),
7889
+ counts: report?.counts ?? { leaked: 0, idle: 0, piling: 0, handle: 0, probable: 0 },
7890
+ profile: profile ? { ...profile, cpu: { ...profile.cpu, series: downsample(profile.cpu.series, REPORT_SERIES_CAP) } } : null,
7891
+ workers: openPagesByWorker(input.censuses),
7892
+ artifactBytes: nonZero(input.artifacts) ?? {},
7893
+ workerHealth: workerHealthOf(input.censuses),
7894
+ timeline: resourceTimeline(input.samples ?? null, input.censuses)
7895
+ };
7896
+ }
7897
+
7898
+ // ../core/src/describe-failure.ts
7899
+ var HEADLINE_MAX_CHARS = 120;
7900
+ var VALUE_MAX_CHARS = 40;
7901
+ var SHORT_VALUE_MAX_CHARS = 20;
7902
+ var MASK_TOKEN_RE = /<(?:N|VALUE|URL|STR|UUID|HASH|EMAIL)>/g;
7903
+ var ACTION_VERBS = {
7904
+ click: "click",
7905
+ dblclick: "double-click",
7906
+ fill: "fill",
7907
+ type: "type",
7908
+ press: "press",
7909
+ pressSequentially: "type",
7910
+ check: "check",
7911
+ uncheck: "uncheck",
7912
+ hover: "hover",
7913
+ tap: "tap",
7914
+ focus: "focus",
7915
+ blur: "blur",
7916
+ clear: "clear",
7917
+ selectOption: "select",
7918
+ selectText: "select text",
7919
+ setInputFiles: "file upload",
7920
+ setChecked: "check",
7921
+ dragTo: "drag",
7922
+ dragAndDrop: "drag",
7923
+ scrollIntoViewIfNeeded: "scroll",
7924
+ screenshot: "screenshot",
7925
+ waitFor: "wait",
7926
+ waitForSelector: "waitForSelector",
7927
+ waitForLoadState: "waitForLoadState",
7928
+ waitForFunction: "waitForFunction",
7929
+ waitForResponse: "waitForResponse",
7930
+ waitForRequest: "waitForRequest",
7931
+ waitForEvent: "waitForEvent",
7932
+ waitForTimeout: "waitForTimeout",
7933
+ innerText: "read text",
7934
+ textContent: "read text",
7935
+ inputValue: "read value",
7936
+ getAttribute: "read attribute",
7937
+ isVisible: "visibility check",
7938
+ isEnabled: "enabled check",
7939
+ isChecked: "checked check",
7940
+ boundingBox: "measure",
7941
+ evaluate: "evaluate",
7942
+ goto: "navigation"
7943
+ };
7944
+ var ACTION_GERUNDS = {
7945
+ click: "clicking",
7946
+ dblclick: "double-clicking",
7947
+ fill: "filling",
7948
+ type: "typing into",
7949
+ press: "pressing a key on",
7950
+ pressSequentially: "typing into",
7951
+ check: "checking",
7952
+ uncheck: "unchecking",
7953
+ hover: "hovering",
7954
+ tap: "tapping",
7955
+ focus: "focusing",
7956
+ clear: "clearing",
7957
+ selectOption: "selecting an option in",
7958
+ setInputFiles: "uploading a file to",
7959
+ dragTo: "dragging",
7960
+ waitFor: "waiting for",
7961
+ waitForSelector: "waiting for",
7962
+ evaluate: "evaluating on"
7963
+ };
7964
+ var STATE_PHRASES = {
7965
+ "not-found": "was not found on the page",
7966
+ hidden: "never became visible",
7967
+ "not-visible": "never became visible",
7968
+ "not-enabled": "never became enabled",
7969
+ "not-editable": "never became editable",
7970
+ "not-stable": "never stopped moving",
7971
+ "outside-viewport": "stayed outside the viewport",
7972
+ "intercepts-pointer": "was covered by another element",
7973
+ detached: "was detached from the DOM",
7974
+ navigating: "was still navigating"
7975
+ };
7976
+ var STATE_MATCHERS = {
7977
+ toBeVisible: { met: "visible", unmet: "never became visible" },
7978
+ toBeHidden: { met: "hidden", unmet: "never became hidden" },
7979
+ toBeEnabled: { met: "enabled", unmet: "never became enabled" },
7980
+ toBeDisabled: { met: "disabled", unmet: "never became disabled" },
7981
+ toBeChecked: { met: "checked", unmet: "never became checked" },
7982
+ toBeEditable: { met: "editable", unmet: "never became editable" },
7983
+ toBeFocused: { met: "focused", unmet: "never received focus" },
7984
+ toBeAttached: { met: "attached", unmet: "never appeared in the DOM" },
7985
+ toBeDetached: { met: "detached", unmet: "never left the DOM" },
7986
+ toBeInViewport: { met: "in the viewport", unmet: "never entered the viewport" },
7987
+ toBeEmpty: { met: "empty", unmet: "never became empty" }
7988
+ };
7989
+ var VALUE_MATCHERS = {
7990
+ toHaveText: "text",
7991
+ toContainText: "text containing",
7992
+ toHaveValue: "value",
7993
+ toHaveValues: "values",
7994
+ toHaveAttribute: "attribute",
7995
+ toHaveClass: "class",
7996
+ toContainClass: "class",
7997
+ toHaveId: "id",
7998
+ toHaveCSS: "CSS",
7999
+ toHaveJSProperty: "property",
8000
+ toHaveAccessibleName: "accessible name",
8001
+ toHaveAccessibleDescription: "accessible description",
8002
+ toHaveRole: "role",
8003
+ toHaveURL: "URL",
8004
+ toHaveTitle: "title",
8005
+ toHaveScreenshot: "screenshot",
8006
+ toMatchAriaSnapshot: "ARIA snapshot"
8007
+ };
8008
+ var NETWORK_ERRORS = {
8009
+ ERR_CONNECTION_REFUSED: "Connection refused loading",
8010
+ ERR_CONNECTION_RESET: "Connection reset loading",
8011
+ ERR_CONNECTION_CLOSED: "Connection closed loading",
8012
+ ERR_CONNECTION_TIMED_OUT: "Connection timed out loading",
8013
+ ERR_TIMED_OUT: "Connection timed out loading",
8014
+ ERR_NAME_NOT_RESOLVED: "DNS lookup failed for",
8015
+ ERR_INTERNET_DISCONNECTED: "No network while loading",
8016
+ ERR_ADDRESS_UNREACHABLE: "Address unreachable loading",
8017
+ ERR_ABORTED: "Navigation aborted loading",
8018
+ ERR_EMPTY_RESPONSE: "Empty response loading",
8019
+ ERR_TOO_MANY_REDIRECTS: "Too many redirects loading",
8020
+ ERR_SSL_PROTOCOL_ERROR: "TLS error loading",
8021
+ ERR_CERT_AUTHORITY_INVALID: "Untrusted certificate loading",
8022
+ ERR_CERT_COMMON_NAME_INVALID: "Certificate name mismatch loading",
8023
+ ERR_CERT_DATE_INVALID: "Expired certificate loading",
8024
+ ERR_BLOCKED_BY_CLIENT: "Request blocked loading",
8025
+ ERR_FAILED: "Request failed loading",
8026
+ ERR_HTTP_RESPONSE_CODE_FAILURE: "HTTP error loading",
8027
+ NS_ERROR_CONNECTION_REFUSED: "Connection refused loading",
8028
+ NS_ERROR_UNKNOWN_HOST: "DNS lookup failed for",
8029
+ NS_ERROR_NET_TIMEOUT: "Connection timed out loading",
8030
+ NS_ERROR_OFFLINE: "No network while loading",
8031
+ NS_ERROR_ABORT: "Navigation aborted loading",
8032
+ NS_BINDING_ABORTED: "Navigation aborted loading"
8033
+ };
8034
+ function formatTimeout(ms) {
8035
+ if (ms < 1e3) return `${ms} ms`;
8036
+ const seconds = ms / 1e3;
8037
+ const rounded = Math.round(seconds * 10) / 10;
8038
+ return `${Number.isInteger(rounded) ? rounded.toFixed(0) : rounded} s`;
8039
+ }
8040
+ function routeOf(url) {
8041
+ try {
8042
+ const parsed = new URL(url);
8043
+ return `${parsed.pathname}${parsed.search}` || "/";
8044
+ } catch {
8045
+ return url;
8046
+ }
8047
+ }
8048
+ function truncateValue(value, max) {
8049
+ const flat = value.replace(/\s+/g, " ").trim();
8050
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
8051
+ }
8052
+ function countNoun(locator, count) {
8053
+ const role = locator ? /getByRole\(\s*['"]([a-z]+)['"]/.exec(locator)?.[1] : null;
8054
+ const noun = role ?? "element";
8055
+ if (count === 1) return noun;
8056
+ return noun.endsWith("s") ? noun : noun.endsWith("x") || noun.endsWith("ch") ? `${noun}es` : `${noun}s`;
5489
8057
  }
5490
8058
  function textOfGetByText(locator) {
5491
8059
  if (!locator) return null;
@@ -5854,7 +8422,7 @@ function testLocation(test) {
5854
8422
  return `${testFile(test)}:${test.location.line}:${test.location.column}`;
5855
8423
  }
5856
8424
  function testFile(test) {
5857
- return path18.relative(process.cwd(), test.location.file).split(path18.sep).join("/");
8425
+ return path23.relative(process.cwd(), test.location.file).split(path23.sep).join("/");
5858
8426
  }
5859
8427
  var PiwiDashboardReporter = class _PiwiDashboardReporter {
5860
8428
  constructor(rawOptions = {}) {
@@ -5882,6 +8450,20 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
5882
8450
  this.filterDetails = null;
5883
8451
  /** Configured `maxFailures` (0 = unlimited) — disambiguates an interrupted run's unrun reason. */
5884
8452
  this.maxFailures = 0;
8453
+ /** The resource censuses the capture fixtures attached, one per test attempt. */
8454
+ this.resourceCensuses = [];
8455
+ /** Lifecycle steps of the tests that ran without the fixtures. */
8456
+ this.lifecycleTallies = [];
8457
+ /** The user fixtures set up by the tests that left a page unused, by test id. */
8458
+ this.fixturesByTest = /* @__PURE__ */ new Map();
8459
+ /** The file workers append their shutdown census to, set in `onBegin`. */
8460
+ this.resourcesFile = null;
8461
+ /** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
8462
+ this.sampler = null;
8463
+ /** Bytes of the files the tests attached, by kind. */
8464
+ this.artifactBytes = emptyArtifacts();
8465
+ /** The run's resource report for the dashboard, built in `onEnd`. */
8466
+ this.resourceReport = null;
5885
8467
  this.streamManager = null;
5886
8468
  /** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
5887
8469
  this.setupSteps = [];
@@ -5941,6 +8523,17 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
5941
8523
  this.startTime = (/* @__PURE__ */ new Date()).toISOString();
5942
8524
  this.playwrightVersion = config.version;
5943
8525
  this.maxFailures = config.maxFailures ?? 0;
8526
+ if (this.options.captureResources !== false) {
8527
+ this.resourcesFile = path23.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
8528
+ process.env[PIWI_RESOURCES_RESULTS_ENV] = this.resourcesFile;
8529
+ try {
8530
+ this.sampler = new RunSampler({ outputDirs: (config.projects ?? []).map((project) => project.outputDir) });
8531
+ this.sampler.start();
8532
+ } catch (error) {
8533
+ this.sampler = null;
8534
+ this.logger.debug(`Run sampler not started: ${errorMessage(error)}`);
8535
+ }
8536
+ }
5944
8537
  this.logger.info(
5945
8538
  `Starting test run for project: ${this.options.projectName} (Playwright v${this.playwrightVersion})`
5946
8539
  );
@@ -6165,6 +8758,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
6165
8758
  }
6166
8759
  }
6167
8760
  }
8761
+ if (this.enabled && this.options.captureResources !== false) this.collectResources(test, result, testCase);
6168
8762
  const reachAttachment = this.options.captureCodeReach === true && this.options.collectPerformanceMetrics !== false ? result.attachments.find((a) => a.name === ATTACHMENT_NAMES.codeReach) : void 0;
6169
8763
  if (reachAttachment?.body) {
6170
8764
  try {
@@ -6209,6 +8803,80 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
6209
8803
  if (this.options.liveFileUploads) this.streamManager.scheduleLiveUpload(testCase);
6210
8804
  }
6211
8805
  }
8806
+ /**
8807
+ * Add up the files a test attached, and keep its resource census; for a test
8808
+ * that ran without the capture fixtures, tally the browsers and contexts its
8809
+ * steps opened and closed.
8810
+ */
8811
+ collectResources(test, result, testCase) {
8812
+ const artifacts = emptyArtifacts();
8813
+ for (const file of result.attachments ?? []) {
8814
+ if (!file.path || INTERNAL_ATTACHMENT_NAMES.has(file.name)) continue;
8815
+ try {
8816
+ const kind = artifactKind(file.name, file.contentType);
8817
+ const bytes = fs21.statSync(file.path).size;
8818
+ artifacts[kind] += bytes;
8819
+ this.artifactBytes[kind] += bytes;
8820
+ } catch {
8821
+ }
8822
+ }
8823
+ const attachment = result.attachments?.find((a) => a.name === ATTACHMENT_NAMES.resources);
8824
+ const census = parseResourceCensus(attachment?.body);
8825
+ if (census) {
8826
+ this.resourceCensuses.push(census);
8827
+ const resources = executionResources(census, artifacts);
8828
+ if (resources) testCase.resources = resources;
8829
+ const leftIdle = census.closed.some((c) => c.used === false) || census.open.some((o) => o.used === false);
8830
+ if (leftIdle) this.fixturesByTest.set(test.id, userFixturesOf(result.steps ?? []));
8831
+ return;
8832
+ }
8833
+ if (!result.steps?.length) return;
8834
+ const tally = tallyLifecycleSteps(testFile(test), result.steps);
8835
+ if (tally.opened.length > 0 || tally.closed.context > 0 || tally.closed.browser > 0)
8836
+ this.lifecycleTallies.push(tally);
8837
+ }
8838
+ /**
8839
+ * Print what the run left open or opened for nothing, from the censuses and
8840
+ * the workers' shutdown file, then what the run cost the machine.
8841
+ */
8842
+ async reportResources() {
8843
+ if (this.options.captureResources === false) return;
8844
+ const profile = this.sampler ? await this.sampler.stop() : null;
8845
+ const samples = this.sampler?.timedSeries() ?? null;
8846
+ this.sampler = null;
8847
+ const censuses = [...this.resourceCensuses];
8848
+ if (this.resourcesFile) {
8849
+ try {
8850
+ censuses.push(...parseResourceResults(fs21.readFileSync(this.resourcesFile, "utf8")));
8851
+ fs21.rmSync(this.resourcesFile, { force: true });
8852
+ } catch {
8853
+ }
8854
+ }
8855
+ const lines = [];
8856
+ try {
8857
+ const report = censuses.length > 0 || this.lifecycleTallies.length > 0 ? buildResourceReport({ censuses, fixturesByTest: this.fixturesByTest, tallies: this.lifecycleTallies }) : null;
8858
+ if (report) lines.push(...formatResourceSummary(report, readLeakCheck(this.options.leakCheck)));
8859
+ if (profile) {
8860
+ lines.push(
8861
+ ...formatMachinePanel(profile, { artifacts: this.artifactBytes, workers: workerHealthOf(censuses) })
8862
+ );
8863
+ }
8864
+ if (report || profile) {
8865
+ this.resourceReport = resourceReportWire({
8866
+ report,
8867
+ profile,
8868
+ samples,
8869
+ censuses,
8870
+ artifacts: this.artifactBytes,
8871
+ shardIndex: this.shardInfo?.current ?? null
8872
+ });
8873
+ }
8874
+ } catch (error) {
8875
+ this.logger.debug(`Resource summary skipped: ${errorMessage(error)}`);
8876
+ }
8877
+ for (const line of lines) this.logger.info(line);
8878
+ emitResourceSummary(lines, this.logger);
8879
+ }
6212
8880
  /**
6213
8881
  * Synthesize `didnotrun` cases for tests Playwright planned but never reported
6214
8882
  * (no `onTestEnd`) — typically because `maxFailures` cut the run short. These
@@ -6258,11 +8926,16 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
6258
8926
  /** Playwright reporter hook: called when the full test run finishes */
6259
8927
  async onEnd(result) {
6260
8928
  if (this.listMode || !this.enabled) return;
8929
+ if (this.startTime === null) {
8930
+ this.logger.debug("The run stopped before any test began \u2014 nothing to report.");
8931
+ return;
8932
+ }
6261
8933
  const unrunReason = resolveUnrunReason(result?.status, {
6262
8934
  maxFailures: this.maxFailures,
6263
8935
  failures: this.failedTests + this.timedOutTests
6264
8936
  });
6265
8937
  this.materializeUnrunTests(unrunReason);
8938
+ await this.reportResources();
6266
8939
  try {
6267
8940
  await this.submitter.submit(
6268
8941
  {
@@ -6282,7 +8955,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
6282
8955
  shardInfo: this.shardInfo,
6283
8956
  setupSteps: this.setupSteps,
6284
8957
  isFullRun: this.isFullRun,
6285
- filterDetails: this.filterDetails
8958
+ filterDetails: this.filterDetails,
8959
+ resourceReport: this.resourceReport
6286
8960
  },
6287
8961
  result
6288
8962
  );
@@ -6366,12 +9040,14 @@ async function resolveSelection(options = {}) {
6366
9040
  }
6367
9041
 
6368
9042
  // src/internal/capture/capture-fixtures.ts
6369
- var path23 = __toESM(require("path"));
9043
+ var path27 = __toESM(require("path"));
6370
9044
  var import_node_zlib2 = require("zlib");
6371
9045
 
6372
9046
  // ../picker-dom/src/probe.ts
6373
9047
  function probeElementAttrs(el, arg) {
6374
9048
  const { keep, tagRoles, inputRoles, roleSources, includeStructural } = arg;
9049
+ const countMatches = arg.countMatches !== false;
9050
+ const testIdAttr = arg.testIdAttribute || "data-testid";
6375
9051
  const FIELD_TAGS = ["input", "select", "textarea"];
6376
9052
  const textWithout = (root, skip) => {
6377
9053
  if (root === skip) return "";
@@ -6396,42 +9072,45 @@ function probeElementAttrs(el, arg) {
6396
9072
  }
6397
9073
  };
6398
9074
  const attrMap = {};
6399
- for (const key of keep) {
9075
+ const keys = arg.testIdAttribute && keep.indexOf(arg.testIdAttribute) === -1 ? [...keep, arg.testIdAttribute] : keep;
9076
+ for (const key of keys) {
6400
9077
  const v = el.getAttribute(key) ?? el[key];
6401
9078
  attrMap[key] = typeof v === "string" ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
6402
9079
  }
6403
9080
  const r = el.getBoundingClientRect();
6404
9081
  const selectorCounts = {};
6405
- try {
6406
- const doc = el.ownerDocument;
6407
- const cssEsc = (s) => doc.defaultView.CSS.escape(s);
6408
- const count = (sel) => {
6409
- try {
6410
- return doc.querySelectorAll(sel).length;
6411
- } catch {
6412
- return void 0;
6413
- }
6414
- };
6415
- if (attrMap["data-testid"]) {
6416
- selectorCounts.testId = count(`[data-testid=${JSON.stringify(attrMap["data-testid"])}]`);
6417
- }
6418
- if (attrMap["id"]) selectorCounts.id = count(`#${cssEsc(attrMap["id"])}`);
6419
- if (attrMap["name"]) selectorCounts.name = count(`[name=${JSON.stringify(attrMap["name"])}]`);
6420
- if (attrMap["placeholder"]) {
6421
- selectorCounts.placeholder = count(`[placeholder=${JSON.stringify(attrMap["placeholder"])}]`);
6422
- }
6423
- if (attrMap["alt"]) selectorCounts.alt = count(`[alt=${JSON.stringify(attrMap["alt"])}]`);
6424
- if (attrMap["title"]) selectorCounts.title = count(`[title=${JSON.stringify(attrMap["title"])}]`);
6425
- const classList = (attrMap["class"] || "").split(/\s+/).filter((c) => c.length > 1).slice(0, 10);
6426
- if (classList.length > 0) {
6427
- const classCounts = {};
6428
- for (const cls of classList) {
6429
- const n = count(`.${cssEsc(cls)}`);
6430
- if (n !== void 0) classCounts[cls] = n;
9082
+ if (countMatches) {
9083
+ try {
9084
+ const doc = el.ownerDocument;
9085
+ const cssEsc = (s) => doc.defaultView.CSS.escape(s);
9086
+ const count = (sel) => {
9087
+ try {
9088
+ return doc.querySelectorAll(sel).length;
9089
+ } catch {
9090
+ return void 0;
9091
+ }
9092
+ };
9093
+ if (attrMap[testIdAttr]) {
9094
+ selectorCounts.testId = count(`[${testIdAttr}=${JSON.stringify(attrMap[testIdAttr])}]`);
9095
+ }
9096
+ if (attrMap["id"]) selectorCounts.id = count(`#${cssEsc(attrMap["id"])}`);
9097
+ if (attrMap["name"]) selectorCounts.name = count(`[name=${JSON.stringify(attrMap["name"])}]`);
9098
+ if (attrMap["placeholder"]) {
9099
+ selectorCounts.placeholder = count(`[placeholder=${JSON.stringify(attrMap["placeholder"])}]`);
9100
+ }
9101
+ if (attrMap["alt"]) selectorCounts.alt = count(`[alt=${JSON.stringify(attrMap["alt"])}]`);
9102
+ if (attrMap["title"]) selectorCounts.title = count(`[title=${JSON.stringify(attrMap["title"])}]`);
9103
+ const classList = (attrMap["class"] || "").split(/\s+/).filter((c) => c.length > 1).slice(0, 10);
9104
+ if (classList.length > 0) {
9105
+ const classCounts = {};
9106
+ for (const cls of classList) {
9107
+ const n = count(`.${cssEsc(cls)}`);
9108
+ if (n !== void 0) classCounts[cls] = n;
9109
+ }
9110
+ selectorCounts.classes = classCounts;
6431
9111
  }
6432
- selectorCounts.classes = classCounts;
9112
+ } catch {
6433
9113
  }
6434
- } catch {
6435
9114
  }
6436
9115
  let rolePosition = null;
6437
9116
  const ancestors = [];
@@ -6448,7 +9127,7 @@ function probeElementAttrs(el, arg) {
6448
9127
  };
6449
9128
  const roleMemo = /* @__PURE__ */ new Map();
6450
9129
  const textMemo = /* @__PURE__ */ new Map();
6451
- const roleOf = (n) => {
9130
+ const roleOf2 = (n) => {
6452
9131
  const cached = roleMemo.get(n);
6453
9132
  if (cached !== void 0) return cached;
6454
9133
  let role;
@@ -6471,7 +9150,7 @@ function probeElementAttrs(el, arg) {
6471
9150
  const al = n.getAttribute("aria-level");
6472
9151
  return al && /^\d+$/.test(al) ? Number(al) : null;
6473
9152
  };
6474
- const targetRole = roleOf(el);
9153
+ const targetRole = roleOf2(el);
6475
9154
  const targetLevel = targetRole === "heading" ? levelOf(el) : null;
6476
9155
  const nameOf = (n) => {
6477
9156
  const labelledBy = !!n.getAttribute("aria-labelledby");
@@ -6520,8 +9199,8 @@ function probeElementAttrs(el, arg) {
6520
9199
  }
6521
9200
  return total;
6522
9201
  };
6523
- const nodes = doc.querySelectorAll(roleSources);
6524
- const nodesUsable = nodes.length <= 4e3;
9202
+ const nodes = countMatches ? doc.querySelectorAll(roleSources) : [];
9203
+ const nodesUsable = countMatches && nodes.length <= 4e3;
6525
9204
  const rolesUsable = !!targetRole && nodesUsable;
6526
9205
  if (rolesUsable) {
6527
9206
  let roleCountAll = 0;
@@ -6531,7 +9210,7 @@ function probeElementAttrs(el, arg) {
6531
9210
  let visibleRoleNameCount = 0;
6532
9211
  for (let i = 0; i < nodes.length; i++) {
6533
9212
  const n = nodes[i];
6534
- if (roleOf(n) !== targetRole) continue;
9213
+ if (roleOf2(n) !== targetRole) continue;
6535
9214
  if (n === el) index = roleCountAll;
6536
9215
  roleCountAll++;
6537
9216
  if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
@@ -6555,17 +9234,18 @@ function probeElementAttrs(el, arg) {
6555
9234
  };
6556
9235
  }
6557
9236
  }
6558
- if (textNeedle) {
9237
+ if (textNeedle && countMatches) {
6559
9238
  const textCount = countTextOwners(doc.body || doc.documentElement, 4e3);
6560
9239
  if (textCount >= 0) selectorCounts.text = textCount;
6561
9240
  }
6562
- if (rolesUsable || textNeedle) {
9241
+ if (rolesUsable || textNeedle || !countMatches) {
6563
9242
  const CONTAINER_TAGS = ["form", "nav", "main", "article", "section", "dialog", "table", "li", "tr"];
6564
9243
  const NOISY_DATA_ATTR = /^data-(v-[0-9a-f]+|reactid|react-checksum|svelte-\w+|ng-\w+|ember\w*)$/i;
6565
9244
  const POSITIONAL_DATA_ATTR = /^data-(index|idx|i|key|row|rownum|col|column|position|pos|order|sort|offset|page)$/i;
6566
9245
  const TEST_DATA_ATTRS2 = ["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"];
9246
+ const testIdName = testIdAttr.toLowerCase();
6567
9247
  const usableDataAttr = (name, value) => {
6568
- if (!name || name.slice(0, 5) !== "data-" || name === "data-testid") return false;
9248
+ if (!name || name.slice(0, 5) !== "data-" || name.toLowerCase() === testIdName) return false;
6569
9249
  if (NOISY_DATA_ATTR.test(name) || POSITIONAL_DATA_ATTR.test(name)) return false;
6570
9250
  return !!value && value.length <= 120;
6571
9251
  };
@@ -6584,7 +9264,7 @@ function probeElementAttrs(el, arg) {
6584
9264
  };
6585
9265
  const docRoleCount = (role) => {
6586
9266
  let c = 0;
6587
- for (let i = 0; i < nodes.length; i++) if (roleOf(nodes[i]) === role) c++;
9267
+ for (let i = 0; i < nodes.length; i++) if (roleOf2(nodes[i]) === role) c++;
6588
9268
  return c;
6589
9269
  };
6590
9270
  const rawText = (n) => (n.textContent || "").replace(/\s+/g, " ").trim();
@@ -6595,7 +9275,13 @@ function probeElementAttrs(el, arg) {
6595
9275
  return digits <= letters;
6596
9276
  };
6597
9277
  const usableDiscriminator = (t) => !!t && t.length <= 60 && t !== targetText && namesRatherThanReports(t);
9278
+ const hasAtMost = (root, cap) => {
9279
+ const walker = doc.createTreeWalker(root, 1);
9280
+ for (let n = 0; walker.nextNode(); n++) if (n >= cap) return false;
9281
+ return true;
9282
+ };
6598
9283
  const discriminatingText = (anc) => {
9284
+ if (!countMatches && !hasAtMost(anc, 200)) return null;
6599
9285
  const heading = anc.querySelector('h1,h2,h3,h4,h5,h6,[role="heading"]');
6600
9286
  if (heading) {
6601
9287
  const t = rawText(heading);
@@ -6616,7 +9302,7 @@ function probeElementAttrs(el, arg) {
6616
9302
  const needle = text.toLowerCase();
6617
9303
  let c = 0;
6618
9304
  for (let i = 0; i < nodes.length; i++) {
6619
- if (roleOf(nodes[i]) !== role) continue;
9305
+ if (roleOf2(nodes[i]) !== role) continue;
6620
9306
  if (normText(nodes[i]).indexOf(needle) !== -1) c++;
6621
9307
  }
6622
9308
  return c;
@@ -6627,7 +9313,7 @@ function probeElementAttrs(el, arg) {
6627
9313
  depth++;
6628
9314
  const tag = (node.tagName || "").toLowerCase();
6629
9315
  if (tag === "body" || tag === "html") break;
6630
- const testId = node.getAttribute("data-testid");
9316
+ const testId = node.getAttribute(testIdAttr);
6631
9317
  const id = node.getAttribute("id");
6632
9318
  const explicitRole = node.getAttribute("role");
6633
9319
  const ariaLabel = node.getAttribute("aria-label");
@@ -6641,16 +9327,16 @@ function probeElementAttrs(el, arg) {
6641
9327
  scopedRoleCount = 0;
6642
9328
  for (let i = 0; i < scoped.length; i++) {
6643
9329
  const n = scoped[i];
6644
- if (roleOf(n) !== targetRole) continue;
9330
+ if (roleOf2(n) !== targetRole) continue;
6645
9331
  if (targetLevel != null && levelOf(n) !== targetLevel) continue;
6646
9332
  scopedRoleCount++;
6647
9333
  }
6648
9334
  }
6649
9335
  }
6650
- const scopedTextCount = countTextOwners(node, 2e3);
9336
+ const scopedTextCount = countMatches ? countTextOwners(node, 2e3) : -1;
6651
9337
  let filterText = null;
6652
9338
  try {
6653
- if (anchorRole && nodesUsable) filterText = discriminatingText(node);
9339
+ if (anchorRole && (nodesUsable || !countMatches)) filterText = discriminatingText(node);
6654
9340
  } catch {
6655
9341
  filterText = null;
6656
9342
  }
@@ -6663,11 +9349,13 @@ function probeElementAttrs(el, arg) {
6663
9349
  ariaLabel: ariaLabel || null,
6664
9350
  ...scopedRoleCount >= 0 ? { scopedRoleCount } : {},
6665
9351
  ...scopedTextCount >= 0 ? { scopedTextCount } : {},
6666
- ...testId ? { testIdCount: count(`[data-testid=${JSON.stringify(testId)}]`) } : {},
6667
- ...id ? { idCount: count(`#${cssEsc(id)}`) } : {},
9352
+ ...testId && countMatches ? { testIdCount: count(`[${testIdAttr}=${JSON.stringify(testId)}]`) } : {},
9353
+ ...id && countMatches ? { idCount: count(`#${cssEsc(id)}`) } : {},
6668
9354
  ...anchorRole && nodesUsable ? { roleCount: docRoleCount(anchorRole) } : {},
6669
- ...filterText ? { filterText, filterRoleCount: filterMatchCount(anchorRole, filterText) } : {},
6670
- ...dataAttr ? { dataAttr, dataAttrCount: count(`[${dataAttr.name}=${JSON.stringify(dataAttr.value)}]`) } : {}
9355
+ ...filterText ? { filterText } : {},
9356
+ ...filterText && countMatches ? { filterRoleCount: filterMatchCount(anchorRole, filterText) } : {},
9357
+ ...dataAttr ? { dataAttr } : {},
9358
+ ...dataAttr && countMatches ? { dataAttrCount: count(`[${dataAttr.name}=${JSON.stringify(dataAttr.value)}]`) } : {}
6671
9359
  });
6672
9360
  }
6673
9361
  node = node.parentElement;
@@ -6705,6 +9393,17 @@ function installPickerOverlay(arg) {
6705
9393
  return;
6706
9394
  }
6707
9395
  const Z = 2147483600;
9396
+ const testIdAttr = arg.testIdAttribute || "data-testid";
9397
+ const words = {
9398
+ banner: "Piwi inspector \u2014 click any element to generate locators for it",
9399
+ keys: "\u2191 parent \xB7 \u2193 child \xB7 Esc skip",
9400
+ keysHovering: "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip",
9401
+ analyzing: "Analyzing element\u2026"
9402
+ };
9403
+ for (const key of Object.keys(words)) {
9404
+ const given = arg.strings?.[key];
9405
+ if (typeof given === "string" && given) words[key] = given;
9406
+ }
6708
9407
  const highlight = doc.createElement("div");
6709
9408
  highlight.id = "__piwi_picker_highlight";
6710
9409
  highlight.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};display:none;box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55),inset 0 0 0 1px rgba(255,255,255,.5);`;
@@ -6729,27 +9428,53 @@ function installPickerOverlay(arg) {
6729
9428
  const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
6730
9429
  const hlLocator = (expr) => `<code style="font-family:${MONO}">${hlTokens(expr)}</code>`;
6731
9430
  const head = doc.createElement("div");
6732
- head.innerHTML = arg.transport === "postMessage" ? "Click an element to generate locators" : arg.failing ? `Piwi locator picker \u2014 click the element that should replace ${hlLocator(arg.failing)}` : "Piwi inspector \u2014 click any element to generate locators for it";
9431
+ if (arg.transport === "postMessage") head.textContent = "Click an element to generate locators";
9432
+ else if (arg.failing)
9433
+ head.innerHTML = `Piwi locator picker \u2014 click the element that should replace ${hlLocator(arg.failing)}`;
9434
+ else head.textContent = words.banner;
6733
9435
  const locatorLine = doc.createElement("div");
6734
9436
  locatorLine.id = "__piwi_picker_locator";
6735
9437
  locatorLine.style.cssText = `display:none;margin-top:7px;padding:5px 9px;border-radius:7px;background:#0b1120;border:1px solid #4c1d95;font:13.5px/1.55 ${MONO};word-break:break-word;overflow-wrap:anywhere;`;
6736
9438
  const foot = doc.createElement("div");
6737
9439
  foot.id = "__piwi_picker_foot";
6738
9440
  foot.style.cssText = "color:#9ca3af;margin-top:6px;font-size:12px;";
6739
- foot.textContent = "\u2191 parent \xB7 \u2193 child \xB7 Esc skip";
9441
+ foot.textContent = words.keys;
6740
9442
  banner.appendChild(head);
6741
9443
  banner.appendChild(locatorLine);
6742
9444
  banner.appendChild(foot);
6743
9445
  const label = doc.createElement("div");
6744
9446
  label.id = "__piwi_picker_label";
6745
9447
  label.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(620px,92vw);background:#0b1120;color:#f9fafb;border:1px solid #7c3aed;border-radius:7px;padding:4px 8px;font:12.5px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
6746
- doc.body.appendChild(highlight);
6747
- doc.body.appendChild(label);
6748
- doc.body.appendChild(banner);
9448
+ const mountParent = () => {
9449
+ let modal = null;
9450
+ try {
9451
+ modal = doc.querySelector("dialog:modal");
9452
+ } catch {
9453
+ modal = null;
9454
+ }
9455
+ if (!modal) return doc.body;
9456
+ const s = g.getComputedStyle(modal);
9457
+ const holdsFixed = s.transform !== "none" || s.perspective !== "none" || s.filter !== "none" || /\b(paint|layout|strict|content)\b/.test(s.contain) || /\b(transform|perspective|filter)\b/.test(s.willChange);
9458
+ return holdsFixed ? doc.body : modal;
9459
+ };
9460
+ const mount = (...nodes) => {
9461
+ const parent = mountParent();
9462
+ for (const node of nodes) parent.appendChild(node);
9463
+ if (parent !== doc.body) {
9464
+ parent.addEventListener(
9465
+ "close",
9466
+ () => {
9467
+ for (const node of nodes) if (node.parentNode === parent) doc.body.appendChild(node);
9468
+ },
9469
+ { once: true }
9470
+ );
9471
+ }
9472
+ };
9473
+ mount(highlight, label, banner);
6749
9474
  const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
6750
9475
  const describe = (el) => {
6751
9476
  const tag = (el.tagName || "?").toLowerCase();
6752
- const testId = el.getAttribute && el.getAttribute("data-testid");
9477
+ const testId = el.getAttribute && el.getAttribute(testIdAttr);
6753
9478
  if (testId) return `getByTestId('${escJs(testId)}')`;
6754
9479
  if (el.labels && el.labels.length > 0) {
6755
9480
  const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
@@ -6784,7 +9509,7 @@ function installPickerOverlay(arg) {
6784
9509
  const el = chain2[i];
6785
9510
  const tag = (el.tagName || "").toLowerCase();
6786
9511
  if (ACTIONABLE_TAGS.includes(tag)) return i;
6787
- if (el.getAttribute && (el.getAttribute("role") || el.getAttribute("data-testid"))) return i;
9512
+ if (el.getAttribute && (el.getAttribute("role") || el.getAttribute(testIdAttr))) return i;
6788
9513
  }
6789
9514
  return 0;
6790
9515
  };
@@ -6840,7 +9565,7 @@ function installPickerOverlay(arg) {
6840
9565
  locatorLine.style.display = "block";
6841
9566
  }
6842
9567
  placeLabel(r);
6843
- foot.textContent = "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip";
9568
+ foot.textContent = words.keysHovering;
6844
9569
  };
6845
9570
  const redescribe = () => {
6846
9571
  labeled = null;
@@ -6903,6 +9628,13 @@ function installPickerOverlay(arg) {
6903
9628
  banner.remove();
6904
9629
  if (g.__piwiRedescribe === redescribe) delete g.__piwiRedescribe;
6905
9630
  };
9631
+ const teardown = () => {
9632
+ removePickingListeners();
9633
+ removeSuppressed();
9634
+ doc.removeEventListener("click", stop, true);
9635
+ doc.removeEventListener("keydown", stop, true);
9636
+ cleanup();
9637
+ };
6906
9638
  const reportPicked = (el) => {
6907
9639
  removePickingListeners();
6908
9640
  highlight.style.display = "none";
@@ -6913,13 +9645,13 @@ function installPickerOverlay(arg) {
6913
9645
  g.__piwiSnapshotExtras?.onPick?.();
6914
9646
  doc.addEventListener("click", stop, true);
6915
9647
  doc.addEventListener("keydown", stop, true);
6916
- foot.textContent = "Analyzing element\u2026";
9648
+ foot.textContent = words.analyzing;
6917
9649
  g.parent.postMessage({ type: "elementPicked", attrs }, "*");
6918
9650
  } else {
6919
9651
  removeSuppressed();
6920
9652
  g.__piwiPickedElement = el;
6921
9653
  g.__piwiPickState = "picked";
6922
- foot.textContent = "Analyzing element\u2026";
9654
+ foot.textContent = words.analyzing;
6923
9655
  }
6924
9656
  };
6925
9657
  const reportSkipped = () => {
@@ -6967,7 +9699,7 @@ function installPickerOverlay(arg) {
6967
9699
  }
6968
9700
  });
6969
9701
  };
6970
- g.__piwiPickCleanup = cleanup;
9702
+ g.__piwiPickCleanup = teardown;
6971
9703
  doc.addEventListener("mousemove", onMove, true);
6972
9704
  doc.addEventListener("click", onClick, true);
6973
9705
  doc.addEventListener("keydown", onKey, true);
@@ -6989,7 +9721,27 @@ function showAnchorPicker(arg) {
6989
9721
  }
6990
9722
  const Z = 2147483600;
6991
9723
  const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
6992
- const roleOf = (n) => {
9724
+ const testIdAttr = arg.testIdAttribute || "data-testid";
9725
+ const words = {
9726
+ title: "Scope to stable parents (optional)",
9727
+ hint: "Pick one or more parents to anchor the locator to. Hover a row to see the parent.",
9728
+ noneSelected: "No parents selected \u2014 standard alternatives only.",
9729
+ matchesOne: "\u2713 Selection matches exactly 1 element",
9730
+ matchesMany: "\u2717 Selection matches {count} elements",
9731
+ countUnavailable: "Match count unavailable",
9732
+ containsOne: "contains exactly 1 matching element",
9733
+ containsMany: "contains {count} matching elements",
9734
+ needsTestId: `add a ${testIdAttr} to make this usable`,
9735
+ noHook: "no stable hook",
9736
+ use: "Use selected parents",
9737
+ skip: "Skip (Esc)"
9738
+ };
9739
+ for (const key of Object.keys(words)) {
9740
+ const given = arg.strings?.[key];
9741
+ if (typeof given === "string" && given) words[key] = given;
9742
+ }
9743
+ const counted = (template, n) => template.replace("{count}", String(n));
9744
+ const roleOf2 = (n) => {
6993
9745
  const explicit = n.getAttribute && n.getAttribute("role");
6994
9746
  if (explicit) return explicit;
6995
9747
  const tag = (n.tagName || "").toLowerCase();
@@ -7004,15 +9756,17 @@ function showAnchorPicker(arg) {
7004
9756
  const al = n.getAttribute && n.getAttribute("aria-level");
7005
9757
  return al && /^\d+$/.test(al) ? Number(al) : null;
7006
9758
  };
9759
+ const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
7007
9760
  const leafMatches = (scope) => {
7008
9761
  try {
7009
- if (leafTestId) return scope.querySelectorAll(`[data-testid=${JSON.stringify(leafTestId)}]`).length;
9762
+ if (leafTestId) return scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(leafTestId)}]`).length;
9763
+ if (!leafCountable) return -1;
7010
9764
  const nodes = scope.querySelectorAll(roleSources);
7011
9765
  if (nodes.length > 2e3) return -1;
7012
9766
  let matched = 0;
7013
9767
  for (let i = 0; i < nodes.length; i++) {
7014
9768
  const n = nodes[i];
7015
- if (roleOf(n) !== leafRole) continue;
9769
+ if (roleOf2(n) !== leafRole) continue;
7016
9770
  if (leafLevel != null && levelOf(n) !== leafLevel) continue;
7017
9771
  matched++;
7018
9772
  }
@@ -7042,12 +9796,12 @@ function showAnchorPicker(arg) {
7042
9796
  depth++;
7043
9797
  const tag = (node.tagName || "").toLowerCase();
7044
9798
  if (tag === "body" || tag === "html") break;
7045
- const testId = node.getAttribute("data-testid");
9799
+ const testId = node.getAttribute(testIdAttr);
7046
9800
  const id = node.getAttribute("id");
7047
9801
  const ariaLabel = node.getAttribute("aria-label");
7048
- const role = roleOf(node);
9802
+ const role = roleOf2(node);
7049
9803
  const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
7050
- if (testId) info.testIdCount = count(`[data-testid=${JSON.stringify(testId)}]`);
9804
+ if (testId) info.testIdCount = count(`[${testIdAttr}=${JSON.stringify(testId)}]`);
7051
9805
  if (id) {
7052
9806
  try {
7053
9807
  info.idCount = count(`#${doc.defaultView.CSS.escape(id)}`);
@@ -7058,7 +9812,7 @@ function showAnchorPicker(arg) {
7058
9812
  let roleCount = 0;
7059
9813
  let labeledCount = 0;
7060
9814
  for (const n of roleNodes) {
7061
- if (roleOf(n) !== role) continue;
9815
+ if (roleOf2(n) !== role) continue;
7062
9816
  roleCount++;
7063
9817
  if (ariaLabel && n.getAttribute && n.getAttribute("aria-label") === ariaLabel) labeledCount++;
7064
9818
  }
@@ -7066,7 +9820,7 @@ function showAnchorPicker(arg) {
7066
9820
  if (ariaLabel) info.labeledRoleCount = labeledCount;
7067
9821
  }
7068
9822
  info.scopedLeafCount = leafMatches(node);
7069
- const hookLabel = testId ? `data-testid="${testId}"` : id ? `#${id}` : ariaLabel && role ? `${role} "${ariaLabel}"` : role ? `role ${role}` : "no stable hook";
9823
+ const hookLabel = testId ? `${testIdAttr}="${testId}"` : id ? `#${id}` : ariaLabel && role ? `${role} "${ariaLabel}"` : role ? `role ${role}` : words.noHook;
7070
9824
  rows.push({
7071
9825
  node,
7072
9826
  info,
@@ -7088,13 +9842,13 @@ function showAnchorPicker(arg) {
7088
9842
  const pr = el.getBoundingClientRect();
7089
9843
  pickedOutline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55);left:${pr.left}px;top:${pr.top}px;width:${pr.width}px;height:${pr.height}px;`;
7090
9844
  const panel = doc.createElement("div");
7091
- panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:340px;max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
9845
+ panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:min(340px,calc(100vw - 56px));max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
7092
9846
  const title = doc.createElement("div");
7093
9847
  title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
7094
- title.textContent = "Scope to stable parents (optional)";
9848
+ title.textContent = words.title;
7095
9849
  const sub = doc.createElement("div");
7096
9850
  sub.style.cssText = "color:#9ca3af;margin-bottom:10px;";
7097
- sub.textContent = "Pick one or more parents to anchor the locator to. Hover a row to see the parent.";
9851
+ sub.textContent = words.hint;
7098
9852
  panel.appendChild(title);
7099
9853
  panel.appendChild(sub);
7100
9854
  const selected = /* @__PURE__ */ new Set();
@@ -7102,12 +9856,12 @@ function showAnchorPicker(arg) {
7102
9856
  footer.style.cssText = "margin:10px 0;font-weight:600;";
7103
9857
  const segMatches = (scope, info) => {
7104
9858
  try {
7105
- if (info.testId) return Array.from(scope.querySelectorAll(`[data-testid=${JSON.stringify(info.testId)}]`));
9859
+ if (info.testId) return Array.from(scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(info.testId)}]`));
7106
9860
  if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
7107
9861
  const nodes = Array.from(scope.querySelectorAll(roleSources));
7108
9862
  if (nodes.length > 2e3) return [];
7109
9863
  return nodes.filter(
7110
- (n) => roleOf(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
9864
+ (n) => roleOf2(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
7111
9865
  );
7112
9866
  } catch {
7113
9867
  return [];
@@ -7126,14 +9880,15 @@ function showAnchorPicker(arg) {
7126
9880
  let total = 0;
7127
9881
  for (const s of scopes) {
7128
9882
  const c = leafMatches(s);
7129
- if (c > 0) total += c;
9883
+ if (c < 0) return -1;
9884
+ total += c;
7130
9885
  if (total > 50) return total;
7131
9886
  }
7132
9887
  return total;
7133
9888
  };
7134
9889
  const refreshFooter = () => {
7135
9890
  if (selected.size === 0) {
7136
- footer.textContent = "No parents selected \u2014 standard alternatives only.";
9891
+ footer.textContent = words.noneSelected;
7137
9892
  footer.style.color = "#9ca3af";
7138
9893
  g.__piwiPickChainCount = void 0;
7139
9894
  return;
@@ -7141,10 +9896,10 @@ function showAnchorPicker(arg) {
7141
9896
  const c = chainCount();
7142
9897
  g.__piwiPickChainCount = c;
7143
9898
  if (c === 1) {
7144
- footer.textContent = "\u2713 Selection matches exactly 1 element";
9899
+ footer.textContent = words.matchesOne;
7145
9900
  footer.style.color = "#4ade80";
7146
9901
  } else {
7147
- footer.textContent = c < 0 ? "Match count unavailable" : `\u2717 Selection matches ${c} elements`;
9902
+ footer.textContent = c < 0 ? words.countUnavailable : counted(words.matchesMany, c);
7148
9903
  footer.style.color = "#fbbf24";
7149
9904
  }
7150
9905
  };
@@ -7161,7 +9916,8 @@ function showAnchorPicker(arg) {
7161
9916
  code.textContent = `<${row.info.tag}> ${row.hookLabel}`;
7162
9917
  const hint = doc.createElement("span");
7163
9918
  hint.style.cssText = "color:#9ca3af;";
7164
- hint.textContent = row.selectable ? row.info.scopedLeafCount === 1 ? "contains exactly 1 matching element" : `contains ${row.info.scopedLeafCount ?? "?"} matching elements` : "add a data-testid to make this usable";
9919
+ const inside = row.info.scopedLeafCount ?? -1;
9920
+ hint.textContent = row.selectable ? inside === 1 ? words.containsOne : inside < 0 ? words.countUnavailable : counted(words.containsMany, inside) : words.needsTestId;
7165
9921
  text.appendChild(code);
7166
9922
  text.appendChild(hint);
7167
9923
  line.appendChild(box);
@@ -7199,6 +9955,7 @@ function showAnchorPicker(arg) {
7199
9955
  outline.remove();
7200
9956
  outlineLabel.remove();
7201
9957
  pickedOutline.remove();
9958
+ if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
7202
9959
  };
7203
9960
  const done = (state) => {
7204
9961
  g.__piwiPickAnchors = state === "done" ? rows.filter((_, i) => selected.has(i)).map((r) => r.info) : [];
@@ -7215,7 +9972,7 @@ function showAnchorPicker(arg) {
7215
9972
  buttonRow.style.cssText = "display:flex;gap:8px;margin-top:4px;";
7216
9973
  const useBtn = doc.createElement("button");
7217
9974
  useBtn.style.cssText = "flex:1;background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer;font:600 12px system-ui;";
7218
- useBtn.textContent = "Use selected parents";
9975
+ useBtn.textContent = words.use;
7219
9976
  useBtn.addEventListener("click", (e) => {
7220
9977
  e.preventDefault();
7221
9978
  e.stopImmediatePropagation();
@@ -7223,7 +9980,7 @@ function showAnchorPicker(arg) {
7223
9980
  });
7224
9981
  const skipBtn = doc.createElement("button");
7225
9982
  skipBtn.style.cssText = "background:none;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:8px 10px;cursor:pointer;font:12px system-ui;";
7226
- skipBtn.textContent = "Skip (Esc)";
9983
+ skipBtn.textContent = words.skip;
7227
9984
  skipBtn.addEventListener("click", (e) => {
7228
9985
  e.preventDefault();
7229
9986
  e.stopImmediatePropagation();
@@ -7235,10 +9992,32 @@ function showAnchorPicker(arg) {
7235
9992
  refreshFooter();
7236
9993
  g.__piwiAnchorCleanup = cleanup;
7237
9994
  doc.addEventListener("keydown", onKey, true);
7238
- doc.body.appendChild(pickedOutline);
7239
- doc.body.appendChild(outline);
7240
- doc.body.appendChild(outlineLabel);
7241
- doc.body.appendChild(panel);
9995
+ const mountParent = () => {
9996
+ let modal = null;
9997
+ try {
9998
+ modal = doc.querySelector("dialog:modal");
9999
+ } catch {
10000
+ modal = null;
10001
+ }
10002
+ if (!modal) return doc.body;
10003
+ const s = g.getComputedStyle(modal);
10004
+ const holdsFixed = s.transform !== "none" || s.perspective !== "none" || s.filter !== "none" || /\b(paint|layout|strict|content)\b/.test(s.contain) || /\b(transform|perspective|filter)\b/.test(s.willChange);
10005
+ return holdsFixed ? doc.body : modal;
10006
+ };
10007
+ const mount = (...nodes) => {
10008
+ const parent = mountParent();
10009
+ for (const node2 of nodes) parent.appendChild(node2);
10010
+ if (parent !== doc.body) {
10011
+ parent.addEventListener(
10012
+ "close",
10013
+ () => {
10014
+ for (const node2 of nodes) if (node2.parentNode === parent) doc.body.appendChild(node2);
10015
+ },
10016
+ { once: true }
10017
+ );
10018
+ }
10019
+ };
10020
+ mount(pickedOutline, outline, outlineLabel, panel);
7242
10021
  }
7243
10022
 
7244
10023
  // ../picker-dom/src/overlay-confirm.ts
@@ -7454,7 +10233,7 @@ function isAutoGenerated(value) {
7454
10233
  return false;
7455
10234
  }
7456
10235
  var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
7457
- function generateAlternatives(attrs) {
10236
+ function generateAlternatives(attrs, options = {}) {
7458
10237
  const alts = [];
7459
10238
  const seen = /* @__PURE__ */ new Set();
7460
10239
  const add = (loc) => {
@@ -7465,12 +10244,14 @@ function generateAlternatives(attrs) {
7465
10244
  };
7466
10245
  const { accessibleName } = attrs;
7467
10246
  const tag = attrs.tagName;
7468
- const role = resolveAriaRole(attrs);
10247
+ const role = options.role !== void 0 ? options.role : resolveAriaRole(attrs);
7469
10248
  const counts = attrs.selectorCounts;
7470
10249
  const isUnique = (n) => n == null || n <= 1;
7471
10250
  const ambiguityPenalty = (n) => n != null && n > 1 ? 45 : 0;
7472
10251
  const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
7473
- const testId = attr(attrs, "data-testid");
10252
+ const testIdAttribute = options.testIdAttribute || "data-testid";
10253
+ const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
10254
+ const testId = attr(attrs, testIdAttribute);
7474
10255
  if (testId && isUnique(counts?.testId)) {
7475
10256
  add({
7476
10257
  locator: `getByTestId('${esc(testId)}')`,
@@ -7529,7 +10310,7 @@ function generateAlternatives(attrs) {
7529
10310
  });
7530
10311
  }
7531
10312
  const id = attr(attrs, "id");
7532
- if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
10313
+ if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
7533
10314
  const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
7534
10315
  add({
7535
10316
  locator: `locator('${esc(selector)}')`,
@@ -7539,7 +10320,7 @@ function generateAlternatives(attrs) {
7539
10320
  });
7540
10321
  }
7541
10322
  const name = attr(attrs, "name");
7542
- if (name && isUnique(counts?.name)) {
10323
+ if (name && selectable("name") && isUnique(counts?.name)) {
7543
10324
  const selector = `[name="${escCssAttrValue(name)}"]`;
7544
10325
  add({
7545
10326
  locator: `locator('${esc(selector)}')`,
@@ -7584,7 +10365,7 @@ function generateAlternatives(attrs) {
7584
10365
  score: scores.testId
7585
10366
  });
7586
10367
  }
7587
- if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
10368
+ if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
7588
10369
  idAnchorDone = true;
7589
10370
  const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
7590
10371
  add({
@@ -7594,7 +10375,7 @@ function generateAlternatives(attrs) {
7594
10375
  score: scores.id
7595
10376
  });
7596
10377
  }
7597
- if (!dataAnchorDone && anc.dataAttr && anc.dataAttrCount === 1) {
10378
+ if (!dataAnchorDone && anc.dataAttr && selectable(anc.dataAttr.name) && anc.dataAttrCount === 1) {
7598
10379
  dataAnchorDone = true;
7599
10380
  const { name: name2, value } = anc.dataAttr;
7600
10381
  const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
@@ -7760,7 +10541,7 @@ function mergeCandidates(base, extra) {
7760
10541
  }
7761
10542
 
7762
10543
  // src/internal/capture/locator-healing.ts
7763
- var path19 = __toESM(require("path"));
10544
+ var path24 = __toESM(require("path"));
7764
10545
 
7765
10546
  // ../core/src/locator-fingerprint.ts
7766
10547
  var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
@@ -8130,10 +10911,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
8130
10911
  }
8131
10912
  let rel = file;
8132
10913
  try {
8133
- rel = path19.relative(process.cwd(), file);
10914
+ rel = path24.relative(process.cwd(), file);
8134
10915
  } catch {
8135
10916
  }
8136
- rel = rel.split(path19.sep).join("/");
10917
+ rel = rel.split(path24.sep).join("/");
8137
10918
  if (rel.startsWith("./")) rel = rel.slice(2);
8138
10919
  return `${rel}:${m[3]}:${m[4]}`;
8139
10920
  }
@@ -8147,7 +10928,7 @@ function collectPageInventoryInPage(maxEntries) {
8147
10928
  const doc = g.document;
8148
10929
  if (!doc || !g.location) return null;
8149
10930
  const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
8150
- const roleOf = (el) => {
10931
+ const roleOf2 = (el) => {
8151
10932
  const explicit = el.getAttribute("role");
8152
10933
  if (explicit) return explicit.toLowerCase();
8153
10934
  const tag = (el.tagName || "").toLowerCase();
@@ -8201,7 +10982,7 @@ function collectPageInventoryInPage(maxEntries) {
8201
10982
  return text;
8202
10983
  };
8203
10984
  const nameOf = (el) => {
8204
- const role = roleOf(el);
10985
+ const role = roleOf2(el);
8205
10986
  const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
8206
10987
  if (!holdsValue(el, role)) chain.push(el.textContent);
8207
10988
  chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
@@ -8219,7 +11000,7 @@ function collectPageInventoryInPage(maxEntries) {
8219
11000
  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"]';
8220
11001
  for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
8221
11002
  if (budget <= 0) break;
8222
- const role = roleOf(el);
11003
+ const role = roleOf2(el);
8223
11004
  const name = nameOf(el);
8224
11005
  if (!name) continue;
8225
11006
  const key = `${role}\0${name}`;
@@ -8344,7 +11125,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
8344
11125
  }
8345
11126
 
8346
11127
  // src/internal/capture/pick-on-failure.ts
8347
- var path20 = __toESM(require("path"));
11128
+ var path25 = __toESM(require("path"));
8348
11129
  var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
8349
11130
  function parseLeafLocatorExpression(rawExpr) {
8350
11131
  const leaf = parseLeafLocatorCall(rawExpr);
@@ -8361,7 +11142,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE2, "");
8361
11142
  const parsed = parseLeafLocatorExpression(line[1].trim());
8362
11143
  if (!parsed) continue;
8363
11144
  const loc = err.location;
8364
- const location = loc ? `${path20.relative(process.cwd(), loc.file).split(path20.sep).join("/")}:${loc.line}:${loc.column}` : null;
11145
+ const location = loc ? `${path25.relative(process.cwd(), loc.file).split(path25.sep).join("/")}:${loc.line}:${loc.column}` : null;
8365
11146
  return { method: parsed.method, args: parsed.args, location };
8366
11147
  }
8367
11148
  return null;
@@ -8509,64 +11290,11 @@ function applyPickToSnapshots(snapshots, pick) {
8509
11290
  return true;
8510
11291
  }
8511
11292
 
8512
- // src/internal/capture/quiet-capture.ts
8513
- var import_node_module = require("module");
8514
- var path21 = __toESM(require("path"));
8515
- var BOXED_PREFIX_EXPORTS = [
8516
- [
8517
- "playwright-core/lib/coreBundle",
8518
- (mod) => mod?.utils?.setBoxedStackPrefixes
8519
- ],
8520
- ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
8521
- ];
8522
- var boxing;
8523
- function resolveBoxing() {
8524
- try {
8525
- const root = findOwnPackageJson(__dirname)?.root;
8526
- if (!root) return null;
8527
- const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
8528
- const playwrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
8529
- const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
8530
- for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
8531
- let set;
8532
- try {
8533
- set = pick(requireFromPlaywright(moduleId));
8534
- } catch {
8535
- continue;
8536
- }
8537
- if (typeof set !== "function") continue;
8538
- return {
8539
- set,
8540
- prefixes: [path21.dirname(playwrightPackageJson), path21.join(root, "dist") + path21.sep]
8541
- };
8542
- }
8543
- } catch {
8544
- }
8545
- return null;
8546
- }
8547
- function boxCaptureFrames(extraPrefixes = []) {
8548
- const first = boxing === void 0;
8549
- if (first) boxing = resolveBoxing();
8550
- if (!boxing) return;
8551
- const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
8552
- if (!first && added.length === 0) return;
8553
- boxing.prefixes.push(...added);
8554
- try {
8555
- boxing.set([...boxing.prefixes]);
8556
- } catch {
8557
- }
8558
- }
8559
- function internalCall(owner, run) {
8560
- const wrap = owner?._wrapApiCall;
8561
- if (typeof wrap !== "function") return run();
8562
- return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
8563
- }
8564
-
8565
11293
  // src/internal/capture/code-reach.ts
8566
11294
  var crypto3 = __toESM(require("crypto"));
8567
- var fs18 = __toESM(require("fs"));
8568
- var path22 = __toESM(require("path"));
8569
- var import_node_child_process2 = require("child_process");
11295
+ var fs22 = __toESM(require("fs"));
11296
+ var path26 = __toESM(require("path"));
11297
+ var import_node_child_process3 = require("child_process");
8570
11298
 
8571
11299
  // ../core/src/source-map.ts
8572
11300
  var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -8821,24 +11549,24 @@ function repositoryRoot(from) {
8821
11549
  if (cachedRepoRoot?.from === from) return cachedRepoRoot.root;
8822
11550
  let root = from;
8823
11551
  try {
8824
- root = (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
11552
+ root = (0, import_node_child_process3.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
8825
11553
  cwd: from,
8826
11554
  encoding: "utf-8",
8827
11555
  stdio: ["ignore", "pipe", "ignore"]
8828
11556
  }).trim();
8829
11557
  } catch {
8830
11558
  }
8831
- cachedRepoRoot = { from, root: path22.resolve(root) };
11559
+ cachedRepoRoot = { from, root: path26.resolve(root) };
8832
11560
  return cachedRepoRoot.root;
8833
11561
  }
8834
11562
  function codeReachRoots(configDir, configured) {
8835
11563
  const repoRoot = repositoryRoot(configDir);
8836
- const roots = configured?.length ? configured.map((r) => path22.resolve(configDir, r)) : [configDir];
11564
+ const roots = configured?.length ? configured.map((r) => path26.resolve(configDir, r)) : [configDir];
8837
11565
  return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
8838
11566
  }
8839
11567
  function isFile(file) {
8840
11568
  try {
8841
- return fs18.statSync(file).isFile();
11569
+ return fs22.statSync(file).isFile();
8842
11570
  } catch {
8843
11571
  return false;
8844
11572
  }
@@ -8846,10 +11574,10 @@ function isFile(file) {
8846
11574
  function resolveSourcePath(source, roots) {
8847
11575
  const normalized = normalizeSourcePath(source);
8848
11576
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
8849
- if (path22.isAbsolute(normalized) && isFile(normalized)) return path22.resolve(normalized);
8850
- const relative11 = normalized.replace(/^\/+/, "");
11577
+ if (path26.isAbsolute(normalized) && isFile(normalized)) return path26.resolve(normalized);
11578
+ const relative13 = normalized.replace(/^\/+/, "");
8851
11579
  for (const root of roots) {
8852
- const candidate = path22.resolve(root, relative11);
11580
+ const candidate = path26.resolve(root, relative13);
8853
11581
  if (isFile(candidate)) return candidate;
8854
11582
  }
8855
11583
  return null;
@@ -8881,7 +11609,7 @@ function fileForUrl(url, roots, bases) {
8881
11609
  return file;
8882
11610
  }
8883
11611
  function relativeToRepo(file, repoRoot) {
8884
- return path22.relative(repoRoot, file).split(path22.sep).join("/");
11612
+ return path26.relative(repoRoot, file).split(path26.sep).join("/");
8885
11613
  }
8886
11614
  function scriptHash(entry) {
8887
11615
  return crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -9071,7 +11799,7 @@ function configuredCodeReachRoots() {
9071
11799
  if (Array.isArray(parsed)) return parsed.filter((r) => typeof r === "string" && r !== "");
9072
11800
  } catch {
9073
11801
  }
9074
- return raw.split(path23.delimiter).filter(Boolean);
11802
+ return raw.split(path27.delimiter).filter(Boolean);
9075
11803
  }
9076
11804
  async function stopSinkCodeReach(sink, closing) {
9077
11805
  const session = sink.codeReach;
@@ -9086,7 +11814,7 @@ async function stopSinkCodeReach(sink, closing) {
9086
11814
  if (!entries) return;
9087
11815
  const info = sink.testInfo;
9088
11816
  const configFile = info?.config.configFile;
9089
- const configDir = configFile ? path23.dirname(configFile) : info?.config.rootDir ?? process.cwd();
11817
+ const configDir = configFile ? path27.dirname(configFile) : info?.config.rootDir ?? process.cwd();
9090
11818
  session.files = await resolveCodeReach(
9091
11819
  entries,
9092
11820
  codeReachRoots(configDir, configuredCodeReachRoots()),
@@ -9102,8 +11830,8 @@ async function drainPendingProbes(capMs) {
9102
11830
  let cap;
9103
11831
  await Promise.race([
9104
11832
  Promise.allSettled(PENDING_PROBES),
9105
- new Promise((resolve6) => {
9106
- cap = setTimeout(resolve6, capMs);
11833
+ new Promise((resolve7) => {
11834
+ cap = setTimeout(resolve7, capMs);
9107
11835
  })
9108
11836
  ]);
9109
11837
  clearTimeout(cap);
@@ -9143,10 +11871,10 @@ async function readWebVitals(page) {
9143
11871
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
9144
11872
  paint[key] = Math.round(entry.startTime);
9145
11873
  }
9146
- const readBuffered = (type, extra) => new Promise((resolve6) => {
11874
+ const readBuffered = (type, extra) => new Promise((resolve7) => {
9147
11875
  try {
9148
11876
  const PO = globalThis.PerformanceObserver;
9149
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve6(null);
11877
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve7(null);
9150
11878
  const out = [];
9151
11879
  const po = new PO((list) => out.push(...list.getEntries()));
9152
11880
  po.observe({ type, buffered: true, ...extra });
@@ -9156,10 +11884,10 @@ async function readWebVitals(page) {
9156
11884
  po.disconnect();
9157
11885
  } catch {
9158
11886
  }
9159
- resolve6(out);
11887
+ resolve7(out);
9160
11888
  }, 0);
9161
11889
  } catch {
9162
- resolve6(null);
11890
+ resolve7(null);
9163
11891
  }
9164
11892
  });
9165
11893
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -9272,12 +12000,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
9272
12000
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
9273
12001
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
9274
12002
  function withTimeout(promise, ms) {
9275
- return new Promise((resolve6, reject) => {
12003
+ return new Promise((resolve7, reject) => {
9276
12004
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
9277
12005
  promise.then(
9278
12006
  (value) => {
9279
12007
  clearTimeout(timer);
9280
- resolve6(value);
12008
+ resolve7(value);
9281
12009
  },
9282
12010
  (error) => {
9283
12011
  clearTimeout(timer);
@@ -9650,6 +12378,7 @@ function instrumentPage(page) {
9650
12378
  const original = factories[method].bind(page);
9651
12379
  factories[method] = (...args) => {
9652
12380
  if (currentSink) currentSink.lastActivePage = page;
12381
+ noteResourceUse(page);
9653
12382
  return wrapLocator(page, original(...args), method, args);
9654
12383
  };
9655
12384
  }
@@ -9836,8 +12565,8 @@ async function flushSink(sink, testInfo) {
9836
12565
  let drainDeadline;
9837
12566
  await Promise.race([
9838
12567
  Promise.allSettled(sink.capturePromises),
9839
- new Promise((resolve6) => {
9840
- drainDeadline = setTimeout(resolve6, 2e3);
12568
+ new Promise((resolve7) => {
12569
+ drainDeadline = setTimeout(resolve7, 2e3);
9841
12570
  })
9842
12571
  ]);
9843
12572
  clearTimeout(drainDeadline);
@@ -9979,6 +12708,22 @@ async function flushSink(sink, testInfo) {
9979
12708
  }
9980
12709
  }
9981
12710
  var piwiFixtures = {
12711
+ // Auto, worker-scoped: the worker's resource ledger. Set up before any hook
12712
+ // (worker fixtures come first), so a `beforeAll` context is in it too, and
12713
+ // torn down after the browser closed, so its census sees what only the
12714
+ // worker's shutdown closed. Depends on `playwright` alone, which launches nothing.
12715
+ piwiResources: [
12716
+ async ({ playwright }, use, workerInfo) => {
12717
+ boxCaptureFrames();
12718
+ startResourceLedger(playwright, workerInfo.workerIndex);
12719
+ try {
12720
+ await use();
12721
+ } finally {
12722
+ stopResourceLedger();
12723
+ }
12724
+ },
12725
+ { scope: "worker", auto: true }
12726
+ ],
9982
12727
  // Worker-scoped: patch the shared browser so every page/context created from
9983
12728
  // it — including by user fixtures that take `browser` directly — is captured.
9984
12729
  browser: [
@@ -10026,6 +12771,7 @@ var piwiFixtures = {
10026
12771
  file: testInfo.file,
10027
12772
  titlePath: testInfo.titlePath
10028
12773
  });
12774
+ activeResourceLedger()?.testStarted(testInfo);
10029
12775
  currentSink = sink;
10030
12776
  try {
10031
12777
  await use();
@@ -10052,6 +12798,7 @@ var piwiFixtures = {
10052
12798
  }
10053
12799
  if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
10054
12800
  await flushSink(sink, testInfo);
12801
+ await recordResourceCensus(testInfo);
10055
12802
  }
10056
12803
  },
10057
12804
  { auto: true }
@@ -10083,13 +12830,13 @@ function extendPiwiFixtures(test) {
10083
12830
  }
10084
12831
 
10085
12832
  // src/internal/ai/ai-fixtures.ts
10086
- var fs20 = __toESM(require("fs"));
10087
- var path26 = __toESM(require("path"));
12833
+ var fs24 = __toESM(require("fs"));
12834
+ var path30 = __toESM(require("path"));
10088
12835
  var import_test = require("@playwright/test");
10089
12836
 
10090
12837
  // src/internal/ai/artifact.ts
10091
- var fs19 = __toESM(require("fs"));
10092
- var path24 = __toESM(require("path"));
12838
+ var fs23 = __toESM(require("fs"));
12839
+ var path28 = __toESM(require("path"));
10093
12840
  var ARTIFACT_VERSION = 1;
10094
12841
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
10095
12842
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -10182,7 +12929,7 @@ function parseEntry(text) {
10182
12929
  function readEntry(file) {
10183
12930
  let text;
10184
12931
  try {
10185
- text = fs19.readFileSync(file, "utf8");
12932
+ text = fs23.readFileSync(file, "utf8");
10186
12933
  } catch (error) {
10187
12934
  if (error.code === "ENOENT") return null;
10188
12935
  throw error;
@@ -10192,28 +12939,28 @@ function readEntry(file) {
10192
12939
  function writeEntry(file, entry) {
10193
12940
  const canonical = serializeEntry(entry);
10194
12941
  return withEntryLock(file, () => {
10195
- if (fs19.existsSync(file) && fs19.readFileSync(file, "utf8") === canonical) return { written: false };
10196
- fs19.mkdirSync(path24.dirname(file), { recursive: true });
12942
+ if (fs23.existsSync(file) && fs23.readFileSync(file, "utf8") === canonical) return { written: false };
12943
+ fs23.mkdirSync(path28.dirname(file), { recursive: true });
10197
12944
  const tmp = `${file}.${process.pid}.tmp`;
10198
- fs19.writeFileSync(tmp, canonical);
10199
- fs19.renameSync(tmp, file);
12945
+ fs23.writeFileSync(tmp, canonical);
12946
+ fs23.renameSync(tmp, file);
10200
12947
  return { written: true };
10201
12948
  });
10202
12949
  }
10203
12950
  function withEntryLock(file, fn) {
10204
12951
  const lock = `${file}.lock`;
10205
- fs19.mkdirSync(path24.dirname(file), { recursive: true });
12952
+ fs23.mkdirSync(path28.dirname(file), { recursive: true });
10206
12953
  const deadline = Date.now() + 5e3;
10207
12954
  for (; ; ) {
10208
12955
  try {
10209
- const fd = fs19.openSync(lock, "wx");
10210
- fs19.closeSync(fd);
12956
+ const fd = fs23.openSync(lock, "wx");
12957
+ fs23.closeSync(fd);
10211
12958
  break;
10212
12959
  } catch (error) {
10213
12960
  if (error.code !== "EEXIST") throw error;
10214
12961
  if (Date.now() > deadline) {
10215
12962
  try {
10216
- fs19.unlinkSync(lock);
12963
+ fs23.unlinkSync(lock);
10217
12964
  } catch {
10218
12965
  }
10219
12966
  }
@@ -10223,7 +12970,7 @@ function withEntryLock(file, fn) {
10223
12970
  return fn();
10224
12971
  } finally {
10225
12972
  try {
10226
- fs19.unlinkSync(lock);
12973
+ fs23.unlinkSync(lock);
10227
12974
  } catch {
10228
12975
  }
10229
12976
  }
@@ -10231,7 +12978,7 @@ function withEntryLock(file, fn) {
10231
12978
 
10232
12979
  // src/internal/ai/keys.ts
10233
12980
  var crypto4 = __toESM(require("crypto"));
10234
- var path25 = __toESM(require("path"));
12981
+ var path29 = __toESM(require("path"));
10235
12982
  var DEFAULT_AI_DIR = "__piwi__";
10236
12983
  function normalizeTemplate(template) {
10237
12984
  return template.trim().replace(/\s+/g, " ").toLowerCase();
@@ -10245,13 +12992,13 @@ function slug(text, maxLength = 40) {
10245
12992
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
10246
12993
  }
10247
12994
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
10248
- return path25.join(path25.dirname(specFile), dir, path25.basename(specFile));
12995
+ return path29.join(path29.dirname(specFile), dir, path29.basename(specFile));
10249
12996
  }
10250
12997
  function entryPath(params) {
10251
12998
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
10252
12999
  const hash = hashTemplate(basis, params.ordinal ?? 0);
10253
13000
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
10254
- return path25.join(entryDir(params.specFile, params.dir), name);
13001
+ return path29.join(entryDir(params.specFile, params.dir), name);
10255
13002
  }
10256
13003
  function parseLocation2(location) {
10257
13004
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -10765,7 +13512,7 @@ function testIdentity(testInfo) {
10765
13512
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
10766
13513
  }
10767
13514
  function relativeToCwd(file) {
10768
- return path26.relative(process.cwd(), file).split(path26.sep).join("/");
13515
+ return path30.relative(process.cwd(), file).split(path30.sep).join("/");
10769
13516
  }
10770
13517
  function recordIntents(intents, entry) {
10771
13518
  const add = (locator, kind) => {
@@ -10792,7 +13539,7 @@ function createAiApi(page, testInfo, config, used, intents) {
10792
13539
  const readSource = () => {
10793
13540
  if (source === null) {
10794
13541
  try {
10795
- source = fs20.readFileSync(testInfo.file, "utf8");
13542
+ source = fs24.readFileSync(testInfo.file, "utf8");
10796
13543
  } catch {
10797
13544
  source = "";
10798
13545
  }