@piwitests/reporter 0.44.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,6 +84,8 @@ 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,
@@ -122,6 +126,8 @@ var PIWI_ENV_KEYS = {
122
126
  captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
123
127
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
124
128
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
129
+ captureResources: "PIWI_CAPTURE_RESOURCES",
130
+ leakCheck: "PIWI_LEAK_CHECK",
125
131
  sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
126
132
  uploadManifest: "PIWI_UPLOAD_MANIFEST",
127
133
  defaultCapture: "PIWI_DEFAULT_CAPTURE",
@@ -156,6 +162,7 @@ var PIWI_FLAKE_ENV = {
156
162
  plan: "PIWI_FLAKE_PLAN",
157
163
  results: "PIWI_FLAKE_RESULTS"
158
164
  };
165
+ var PIWI_RESOURCES_RESULTS_ENV = "PIWI_RESOURCES_RESULTS";
159
166
  function readBool(val) {
160
167
  if (val === void 0) return void 0;
161
168
  return val === "true";
@@ -184,6 +191,8 @@ var ENV_FALLBACK_SPECS = [
184
191
  { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
185
192
  { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
186
193
  { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
194
+ { option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
195
+ { option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
187
196
  { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
188
197
  { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
189
198
  { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
@@ -250,6 +259,8 @@ function applyOptionsToEnv(options) {
250
259
  if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
251
260
  env[PIWI_ENV_KEYS.captureServerTraces] = "false";
252
261
  else if (options.captureServerTraces === true) env[PIWI_ENV_KEYS.captureServerTraces] = "true";
262
+ if (options.captureResources !== void 0) env[PIWI_ENV_KEYS.captureResources] = String(options.captureResources);
263
+ if (options.leakCheck !== void 0) env[PIWI_ENV_KEYS.leakCheck] = options.leakCheck;
253
264
  if (options.sampleAriaOnPass === false) env[PIWI_ENV_KEYS.sampleAriaOnPass] = "false";
254
265
  else if (options.sampleAriaOnPass === true) env[PIWI_ENV_KEYS.sampleAriaOnPass] = "true";
255
266
  if (options.inspectOnFailure !== void 0) env[PIWI_ENV_KEYS.inspectOnFailure] = String(options.inspectOnFailure);
@@ -490,7 +501,7 @@ var HttpClient = class {
490
501
  }
491
502
  /** Unified request core: transport, headers, auth, response accumulation, timeout. */
492
503
  request(method, pathname, opts) {
493
- return new Promise((resolve6, reject) => {
504
+ return new Promise((resolve7, reject) => {
494
505
  if (this.closedReason !== null) {
495
506
  reject(new Error(this.closedReason));
496
507
  return;
@@ -513,7 +524,7 @@ var HttpClient = class {
513
524
  data += chunk;
514
525
  });
515
526
  res.on("end", () => {
516
- resolve6({ status: res.statusCode ?? 0, text: data, headers: res.headers });
527
+ resolve7({ status: res.statusCode ?? 0, text: data, headers: res.headers });
517
528
  });
518
529
  res.on("error", reject);
519
530
  res.on("close", () => {
@@ -608,6 +619,7 @@ function toWireTestCase(tc) {
608
619
  locatorSnapshots: rest.locatorSnapshots || null,
609
620
  locatorPages: rest.locatorPages || null,
610
621
  codeReach: rest.codeReach || null,
622
+ resources: rest.resources ?? null,
611
623
  didNotRunReason: rest.didNotRunReason ?? null,
612
624
  expectedStatus: rest.expectedStatus ?? null,
613
625
  blockedBy: rest.blockedBy ?? null
@@ -674,6 +686,7 @@ function serializeRun(payload, opts) {
674
686
  isFullRun: payload.isFullRun ?? true,
675
687
  filterDetails: payload.filterDetails ?? null
676
688
  };
689
+ if (payload.resourceReport) body.resourceReport = payload.resourceReport;
677
690
  if (opts.includeTestCases) {
678
691
  body.testCases = payload.testCases.map((tc) => toWireTestCase(tc));
679
692
  }
@@ -1056,7 +1069,8 @@ var ATTACHMENT_NAMES = {
1056
1069
  pageState: "piwi-page-state",
1057
1070
  userPick: "piwi-user-pick",
1058
1071
  aiUsage: "piwi-ai-usage",
1059
- aiMeta: "piwi-ai-meta"
1072
+ aiMeta: "piwi-ai-meta",
1073
+ resources: "piwi-resources"
1060
1074
  };
1061
1075
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
1062
1076
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
@@ -1277,8 +1291,8 @@ var FileHandler = class {
1277
1291
  }
1278
1292
  if (!lastPath) return null;
1279
1293
  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);
1294
+ await new Promise((resolve7, reject) => {
1295
+ fs6.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve7).on("error", reject);
1282
1296
  });
1283
1297
  return {
1284
1298
  tracePath: lastPath,
@@ -1709,10 +1723,10 @@ function createLimiter(maxConcurrent) {
1709
1723
  if (run) run();
1710
1724
  };
1711
1725
  return function limit(fn) {
1712
- return new Promise((resolve6, reject) => {
1726
+ return new Promise((resolve7, reject) => {
1713
1727
  const run = () => {
1714
1728
  active++;
1715
- Promise.resolve().then(fn).then(resolve6, reject).finally(next);
1729
+ Promise.resolve().then(fn).then(resolve7, reject).finally(next);
1716
1730
  };
1717
1731
  if (active < limitValue) run();
1718
1732
  else queue.push(run);
@@ -2151,7 +2165,7 @@ var StreamManager = class {
2151
2165
  this.logger.debugError(
2152
2166
  `${this.pendingEvents.length} events pending, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`
2153
2167
  );
2154
- await new Promise((resolve6) => setTimeout(resolve6, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
2168
+ await new Promise((resolve7) => setTimeout(resolve7, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
2155
2169
  }
2156
2170
  if (!this.pendingEvents.isEmpty) {
2157
2171
  const remaining = this.pendingEvents.takeAll();
@@ -2208,7 +2222,7 @@ var StreamManager = class {
2208
2222
  if (flush) await flush;
2209
2223
  const delays = [0, 1e3, 3e3];
2210
2224
  for (let attempt = 0; attempt < delays.length; attempt++) {
2211
- if (delays[attempt]) await new Promise((resolve6) => setTimeout(resolve6, delays[attempt]));
2225
+ if (delays[attempt]) await new Promise((resolve7) => setTimeout(resolve7, delays[attempt]));
2212
2226
  try {
2213
2227
  await this.limitLiveUpload(
2214
2228
  () => this.uploader.uploadCaseFiles(
@@ -3900,8 +3914,8 @@ function parseRouteKey(routeKey2) {
3900
3914
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
3901
3915
  }
3902
3916
  function patternToRegex(pattern) {
3903
- const path27 = pattern.split("?")[0] ?? pattern;
3904
- const escaped = path27.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
3917
+ const path31 = pattern.split("?")[0] ?? pattern;
3918
+ const escaped = path31.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
3905
3919
  return new RegExp(`^${escaped}$`);
3906
3920
  }
3907
3921
  function suitePathEqual(a, b) {
@@ -3924,13 +3938,13 @@ function matchProbeItem(plan, test) {
3924
3938
  function requestMatchesRoute(method, url, routeKey2) {
3925
3939
  const { method: rm, pattern } = parseRouteKey(routeKey2);
3926
3940
  if (rm && rm !== method.toUpperCase()) return false;
3927
- let path27;
3941
+ let path31;
3928
3942
  try {
3929
- path27 = new URL(url).pathname;
3943
+ path31 = new URL(url).pathname;
3930
3944
  } catch {
3931
- path27 = url.split("?")[0] ?? url;
3945
+ path31 = url.split("?")[0] ?? url;
3932
3946
  }
3933
- return patternToRegex(pattern).test(path27);
3947
+ return patternToRegex(pattern).test(path31);
3934
3948
  }
3935
3949
  function createProbeState() {
3936
3950
  return { navigated: false, matches: 0 };
@@ -3952,10 +3966,10 @@ function isProbeMode() {
3952
3966
  function loadPlan() {
3953
3967
  if (planCache !== void 0) return planCache;
3954
3968
  planCache = null;
3955
- const path27 = process.env[PIWI_PROBE_ENV.plan];
3956
- if (path27) {
3969
+ const path31 = process.env[PIWI_PROBE_ENV.plan];
3970
+ if (path31) {
3957
3971
  try {
3958
- planCache = JSON.parse(fs14.readFileSync(path27, "utf8"));
3972
+ planCache = JSON.parse(fs14.readFileSync(path31, "utf8"));
3959
3973
  } catch {
3960
3974
  planCache = null;
3961
3975
  }
@@ -3977,10 +3991,10 @@ function outcomeFromStatus(status, applied) {
3977
3991
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
3978
3992
  }
3979
3993
  function recordProbeOutcome(line) {
3980
- const path27 = process.env[PIWI_PROBE_ENV.results];
3981
- if (!path27) return;
3994
+ const path31 = process.env[PIWI_PROBE_ENV.results];
3995
+ if (!path31) return;
3982
3996
  try {
3983
- fs14.appendFileSync(path27, `${JSON.stringify(line)}
3997
+ fs14.appendFileSync(path31, `${JSON.stringify(line)}
3984
3998
  `);
3985
3999
  } catch {
3986
4000
  }
@@ -4364,7 +4378,7 @@ function originOf(url) {
4364
4378
  return null;
4365
4379
  }
4366
4380
  }
4367
- var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, Math.max(0, ms)));
4381
+ var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, Math.max(0, ms)));
4368
4382
  async function performRouteAction(route, action, startedAt) {
4369
4383
  switch (action.kind) {
4370
4384
  case "abort":
@@ -5061,6 +5075,15 @@ function emitGitLabDotenv(output, env, logger) {
5061
5075
  logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
5062
5076
  }
5063
5077
  }
5078
+ function emitResourceSummary(lines, logger, env = process.env) {
5079
+ if (!env.GITHUB_ACTIONS || !env.GITHUB_STEP_SUMMARY || lines.length === 0) return;
5080
+ appendFileLines(
5081
+ env.GITHUB_STEP_SUMMARY,
5082
+ ["### Piwi resources", "", "```text", ...lines, "```", ""],
5083
+ logger,
5084
+ "step summary"
5085
+ );
5086
+ }
5064
5087
  function appendFileLines(file, lines, logger, label) {
5065
5088
  try {
5066
5089
  fs17.appendFileSync(file, lines.join("\n") + "\n");
@@ -5212,7 +5235,8 @@ var RunSubmitter = class {
5212
5235
  shardIndex: run.shardInfo?.current,
5213
5236
  shardTotal: run.shardInfo?.total,
5214
5237
  isFullRun: run.isFullRun,
5215
- filterDetails: run.filterDetails
5238
+ filterDetails: run.filterDetails,
5239
+ resourceReport: run.resourceReport ?? null
5216
5240
  };
5217
5241
  }
5218
5242
  async tryFinishStreaming(run, overallStatus, duration, auth) {
@@ -5243,6 +5267,7 @@ var RunSubmitter = class {
5243
5267
  isFullRun: run.isFullRun,
5244
5268
  filterDetails: run.filterDetails ?? null
5245
5269
  };
5270
+ if (run.resourceReport) finishBody.resourceReport = run.resourceReport;
5246
5271
  if (run.shardInfo) {
5247
5272
  finishBody.shardIndex = run.shardInfo.current;
5248
5273
  finishBody.shardTotal = run.shardInfo.total;
@@ -5327,202 +5352,2269 @@ var RunSubmitter = class {
5327
5352
  }
5328
5353
  };
5329
5354
 
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`;
5355
+ // src/internal/capture/resource-ledger.ts
5356
+ var fs19 = __toESM(require("fs"));
5357
+ var path20 = __toESM(require("path"));
5358
+ var import_node_module2 = require("module");
5359
+
5360
+ // src/internal/capture/quiet-capture.ts
5361
+ var import_node_module = require("module");
5362
+ var path18 = __toESM(require("path"));
5363
+ var BOXED_PREFIX_EXPORTS = [
5364
+ [
5365
+ "playwright-core/lib/coreBundle",
5366
+ (mod) => mod?.utils?.setBoxedStackPrefixes
5367
+ ],
5368
+ ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
5369
+ ];
5370
+ var boxing;
5371
+ var resolvedPlaywrightPackageJson;
5372
+ function resolvePlaywrightPackageJson() {
5373
+ if (resolvedPlaywrightPackageJson !== void 0) return resolvedPlaywrightPackageJson;
5374
+ try {
5375
+ const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
5376
+ resolvedPlaywrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
5377
+ } catch {
5378
+ resolvedPlaywrightPackageJson = null;
5379
+ }
5380
+ return resolvedPlaywrightPackageJson;
5471
5381
  }
5472
- function routeOf(url) {
5382
+ function resolveBoxing() {
5473
5383
  try {
5474
- const parsed = new URL(url);
5475
- return `${parsed.pathname}${parsed.search}` || "/";
5384
+ const root = findOwnPackageJson(__dirname)?.root;
5385
+ if (!root) return null;
5386
+ const playwrightPackageJson = resolvePlaywrightPackageJson();
5387
+ if (!playwrightPackageJson) return null;
5388
+ const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
5389
+ for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
5390
+ let set;
5391
+ try {
5392
+ set = pick(requireFromPlaywright(moduleId));
5393
+ } catch {
5394
+ continue;
5395
+ }
5396
+ if (typeof set !== "function") continue;
5397
+ return {
5398
+ set,
5399
+ prefixes: [path18.dirname(playwrightPackageJson), path18.join(root, "dist") + path18.sep]
5400
+ };
5401
+ }
5476
5402
  } catch {
5477
- return url;
5478
5403
  }
5404
+ return null;
5479
5405
  }
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;
5406
+ function boxCaptureFrames(extraPrefixes = []) {
5407
+ const first = boxing === void 0;
5408
+ if (first) boxing = resolveBoxing();
5409
+ if (!boxing) return;
5410
+ const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
5411
+ if (!first && added.length === 0) return;
5412
+ boxing.prefixes.push(...added);
5413
+ try {
5414
+ boxing.set([...boxing.prefixes]);
5415
+ } catch {
5416
+ }
5483
5417
  }
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`;
5418
+ function internalCall(owner, run) {
5419
+ const wrap = owner?._wrapApiCall;
5420
+ if (typeof wrap !== "function") return run();
5421
+ return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
5489
5422
  }
5490
- function textOfGetByText(locator) {
5491
- if (!locator) return null;
5492
- const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
5493
- return m ? m[2] : null;
5423
+
5424
+ // src/internal/capture/worker-metrics.ts
5425
+ var import_node_perf_hooks = require("perf_hooks");
5426
+
5427
+ // src/internal/support/system-readers.ts
5428
+ var fs18 = __toESM(require("fs"));
5429
+ var path19 = __toESM(require("path"));
5430
+ var CLK_TCK = 100;
5431
+ function readText(file) {
5432
+ try {
5433
+ return fs18.readFileSync(file, "utf8");
5434
+ } catch {
5435
+ return null;
5436
+ }
5494
5437
  }
5495
- function receivedDisplay(parsed) {
5496
- const received = parsed.received ?? "";
5497
- if (parsed.assertion !== "toHaveURL") return received;
5498
- const m = /^"?(https?:\/\/[^"\s]+)"?$/.exec(received);
5499
- return m ? `"${routeOf(m[1])}"` : received;
5438
+ function parseProcStat(pid, text) {
5439
+ const open = text.indexOf("(");
5440
+ const close = text.lastIndexOf(")");
5441
+ if (open < 0 || close < open) return null;
5442
+ const fields = text.slice(close + 2).trim().split(/\s+/);
5443
+ const ppid = Number(fields[1]);
5444
+ const utime = Number(fields[11]);
5445
+ const stime = Number(fields[12]);
5446
+ const start = Number(fields[19]);
5447
+ if (![ppid, utime, stime, start].every(Number.isFinite)) return null;
5448
+ return { pid, ppid, comm: text.slice(open + 1, close), cpuMs: (utime + stime) * 1e3 / CLK_TCK, start };
5449
+ }
5450
+ function parseSchedstat(text) {
5451
+ const waitNs = Number(text.trim().split(/\s+/)[1]);
5452
+ return Number.isFinite(waitNs) ? waitNs / 1e6 : null;
5453
+ }
5454
+ function parseKbField(text, key) {
5455
+ const match = new RegExp(`^${key}:\\s+(\\d+)`, "m").exec(text);
5456
+ return match ? Number(match[1]) : null;
5457
+ }
5458
+ function parseCpuTicks(text) {
5459
+ const line = text.split("\n", 1)[0] ?? "";
5460
+ if (!line.startsWith("cpu ")) return null;
5461
+ const v = line.trim().split(/\s+/).slice(1, 9).map(Number);
5462
+ if (v.length < 8 || v.some((n) => !Number.isFinite(n))) return null;
5463
+ const [user, nice, system, idle, iowait, irq, softirq, steal] = v;
5464
+ return { busy: user + nice + system + irq + softirq, idle, iowait, steal, total: v.reduce((sum, n) => sum + n, 0) };
5465
+ }
5466
+ function parsePsiTotal(text) {
5467
+ const match = /^some\b.*\btotal=(\d+)/m.exec(text);
5468
+ return match ? Number(match[1]) : null;
5469
+ }
5470
+ function parsePsTime(text) {
5471
+ const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/.exec(text.trim());
5472
+ if (!match) return null;
5473
+ const [, days, hours, minutes, seconds] = match;
5474
+ return ((Number(days ?? 0) * 24 + Number(hours ?? 0)) * 3600 + Number(minutes) * 60 + Number(seconds)) * 1e3;
5500
5475
  }
5501
- function firstLineFallback(parsed) {
5502
- const line = (parsed.messageHead.split("\n")[0] ?? "").replace(/^Error:\s*/, "").replace(MASK_TOKEN_RE, "\u2026");
5503
- return truncateValue(line || "Unknown error", HEADLINE_MAX_CHARS);
5476
+ function parsePsOutput(text) {
5477
+ const out = [];
5478
+ for (const line of text.split("\n")) {
5479
+ const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
5480
+ if (!match) continue;
5481
+ const cpuMs = parsePsTime(match[4]);
5482
+ if (cpuMs === null) continue;
5483
+ out.push({
5484
+ pid: Number(match[1]),
5485
+ ppid: Number(match[2]),
5486
+ rssKb: Number(match[3]),
5487
+ cpuMs,
5488
+ command: match[5].trim()
5489
+ });
5490
+ }
5491
+ return out;
5504
5492
  }
5505
- var Line = class {
5506
- constructor() {
5507
- this.parts = [];
5493
+ function roleOf(comm, cmdline) {
5494
+ const command = `${comm} ${cmdline}`;
5495
+ if (/workerProcessEntry/.test(cmdline)) return "worker";
5496
+ if (/loaderProcessEntry/.test(cmdline)) return "runner";
5497
+ if (/(^|[\s/])ffmpeg/.test(command)) return "ffmpeg";
5498
+ const type = /--type=([\w-]+)/.exec(cmdline)?.[1];
5499
+ if (/chrom|headless_shell|msedge|Google Chrome/i.test(command)) {
5500
+ if (!type) return "browser";
5501
+ if (type === "renderer") return "renderer";
5502
+ if (type === "gpu-process") return "gpu";
5503
+ return "utility";
5504
+ }
5505
+ if (/firefox/i.test(command)) return /-contentproc/.test(cmdline) ? "renderer" : "browser";
5506
+ if (/WebKitWebProcess/.test(command)) return "renderer";
5507
+ if (/WebKitGPUProcess/.test(command)) return "gpu";
5508
+ if (/WebKitNetworkProcess/.test(command)) return "utility";
5509
+ if (/MiniBrowser|pw_run\.sh/.test(command)) return "browser";
5510
+ return null;
5511
+ }
5512
+ function descendantsOf(root, table) {
5513
+ const children = /* @__PURE__ */ new Map();
5514
+ for (const proc of table) {
5515
+ const list = children.get(proc.ppid) ?? [];
5516
+ list.push(proc);
5517
+ children.set(proc.ppid, list);
5508
5518
  }
5509
- text(text) {
5510
- if (!text) return this;
5511
- const last = this.parts[this.parts.length - 1];
5512
- if (last && last.kind === "text") last.text += text;
5513
- else this.parts.push({ kind: "text", text });
5514
- return this;
5519
+ const out = [];
5520
+ const stack = [root];
5521
+ const seen = /* @__PURE__ */ new Set([root]);
5522
+ while (stack.length > 0) {
5523
+ for (const child of children.get(stack.pop()) ?? []) {
5524
+ if (seen.has(child.pid)) continue;
5525
+ seen.add(child.pid);
5526
+ out.push(child);
5527
+ stack.push(child.pid);
5528
+ }
5515
5529
  }
5516
- locator(text) {
5517
- this.parts.push({ kind: "locator", text });
5518
- return this;
5530
+ return out;
5531
+ }
5532
+ var ProcFs = class {
5533
+ constructor(root = "/proc") {
5534
+ this.root = root;
5519
5535
  }
5520
- value(text) {
5521
- this.parts.push({ kind: "value", text });
5522
- return this;
5536
+ /** Whether this looks like a Linux `/proc` at all. */
5537
+ available() {
5538
+ return fs18.existsSync(path19.join(this.root, "stat"));
5523
5539
  }
5524
- toString() {
5525
- return this.parts.map((p) => p.text).join("");
5540
+ /** Every process, keyed by pid. */
5541
+ table() {
5542
+ const out = /* @__PURE__ */ new Map();
5543
+ let names;
5544
+ try {
5545
+ names = fs18.readdirSync(this.root);
5546
+ } catch {
5547
+ return out;
5548
+ }
5549
+ for (const name of names) {
5550
+ if (!/^\d+$/.test(name)) continue;
5551
+ const text = readText(path19.join(this.root, name, "stat"));
5552
+ const stat = text ? parseProcStat(Number(name), text) : null;
5553
+ if (stat) out.set(stat.pid, stat);
5554
+ }
5555
+ return out;
5556
+ }
5557
+ stat(pid) {
5558
+ const text = readText(path19.join(this.root, String(pid), "stat"));
5559
+ return text ? parseProcStat(pid, text) : null;
5560
+ }
5561
+ runWaitMs(pid) {
5562
+ const text = readText(path19.join(this.root, String(pid), "schedstat"));
5563
+ return text ? parseSchedstat(text) : null;
5564
+ }
5565
+ cmdline(pid) {
5566
+ return (readText(path19.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
5567
+ }
5568
+ /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
5569
+ statusKb(pid, key) {
5570
+ const text = readText(path19.join(this.root, String(pid), "status"));
5571
+ return text ? parseKbField(text, key) : null;
5572
+ }
5573
+ /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
5574
+ async pssKb(pid) {
5575
+ try {
5576
+ return parseKbField(await fs18.promises.readFile(path19.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
5577
+ } catch {
5578
+ return null;
5579
+ }
5580
+ }
5581
+ /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
5582
+ resetPeakRss(pid) {
5583
+ try {
5584
+ fs18.writeFileSync(path19.join(this.root, String(pid), "clear_refs"), "5");
5585
+ return true;
5586
+ } catch {
5587
+ return false;
5588
+ }
5589
+ }
5590
+ cpuTicks() {
5591
+ const text = readText(path19.join(this.root, "stat"));
5592
+ return text ? parseCpuTicks(text) : null;
5593
+ }
5594
+ /** Total stall time of a resource's pressure file, in µs. */
5595
+ pressureUs(resource) {
5596
+ const text = readText(path19.join(this.root, "pressure", resource));
5597
+ return text ? parsePsiTotal(text) : null;
5598
+ }
5599
+ memAvailableKb() {
5600
+ const text = readText(path19.join(this.root, "meminfo"));
5601
+ return text ? parseKbField(text, "MemAvailable") : null;
5602
+ }
5603
+ /** Open file descriptors of a process. */
5604
+ fdCount(pid) {
5605
+ try {
5606
+ return fs18.readdirSync(path19.join(this.root, String(pid), "fd")).length;
5607
+ } catch {
5608
+ return null;
5609
+ }
5610
+ }
5611
+ };
5612
+ var UNLIMITED = 2 ** 60;
5613
+ function numberIn(text, pattern) {
5614
+ if (text === null) return null;
5615
+ const raw = pattern ? pattern.exec(text)?.[1] : text.trim();
5616
+ const value = Number(raw);
5617
+ return raw !== void 0 && raw !== "" && Number.isFinite(value) ? value : null;
5618
+ }
5619
+ var CgroupFs = class {
5620
+ constructor(procRoot = "/proc", cgroupRoot = "/sys/fs/cgroup", pid = "self") {
5621
+ this.procRoot = procRoot;
5622
+ this.cgroupRoot = cgroupRoot;
5623
+ this.pid = pid;
5624
+ }
5625
+ /** The directory of a controller (v1) or of the unified hierarchy (v2), when it exists. */
5626
+ dirs() {
5627
+ const text = readText(path19.join(this.procRoot, String(this.pid), "cgroup"));
5628
+ if (!text) return null;
5629
+ const v1 = /* @__PURE__ */ new Map();
5630
+ let v2 = null;
5631
+ for (const line of text.split("\n")) {
5632
+ const match = /^\d+:([^:]*):(.*)$/.exec(line.trim());
5633
+ if (!match) continue;
5634
+ if (match[1] === "") v2 = match[2];
5635
+ else for (const controller of match[1].split(",")) v1.set(controller, match[2]);
5636
+ }
5637
+ const pick = (mount, sub) => {
5638
+ const nested = path19.join(mount, sub);
5639
+ if (fs18.existsSync(nested)) return nested;
5640
+ return fs18.existsSync(mount) ? mount : null;
5641
+ };
5642
+ if (v1.has("memory") || v1.has("cpu")) {
5643
+ const memoryPath = v1.get("memory");
5644
+ const cpuPath = v1.get("cpu");
5645
+ const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path19.join(this.cgroupRoot, name)).find((dir) => fs18.existsSync(dir));
5646
+ return {
5647
+ version: 1,
5648
+ memory: memoryPath !== void 0 ? pick(path19.join(this.cgroupRoot, "memory"), memoryPath) : null,
5649
+ cpu: cpuPath !== void 0 && cpuMount ? pick(cpuMount, cpuPath) : null
5650
+ };
5651
+ }
5652
+ if (v2 !== null && fs18.existsSync(path19.join(this.cgroupRoot, "cgroup.controllers"))) {
5653
+ const dir = pick(this.cgroupRoot, v2);
5654
+ return { version: 2, memory: dir, cpu: dir };
5655
+ }
5656
+ return null;
5657
+ }
5658
+ read() {
5659
+ const dirs = this.dirs();
5660
+ if (!dirs) return null;
5661
+ const file = (dir, name) => dir ? readText(path19.join(dir, name)) : null;
5662
+ if (dirs.version === 2) {
5663
+ const max = file(dirs.memory, "memory.max");
5664
+ const cpuMax = file(dirs.cpu, "cpu.max")?.trim().split(/\s+/);
5665
+ const quota = cpuMax && cpuMax[0] !== "max" ? Number(cpuMax[0]) / Number(cpuMax[1]) : null;
5666
+ const throttledUs = numberIn(file(dirs.cpu, "cpu.stat"), /^throttled_usec (\d+)/m);
5667
+ return {
5668
+ version: 2,
5669
+ memoryLimitBytes: max === null || max.trim() === "max" ? null : numberIn(max),
5670
+ memoryCurrentBytes: numberIn(file(dirs.memory, "memory.current")),
5671
+ memoryPeakBytes: numberIn(file(dirs.memory, "memory.peak")),
5672
+ oomKills: numberIn(file(dirs.memory, "memory.events"), /^oom_kill (\d+)/m),
5673
+ cpuQuotaCores: quota !== null && Number.isFinite(quota) ? quota : null,
5674
+ throttledMs: throttledUs === null ? null : throttledUs / 1e3
5675
+ };
5676
+ }
5677
+ const limit = numberIn(file(dirs.memory, "memory.limit_in_bytes"));
5678
+ const quotaUs = numberIn(file(dirs.cpu, "cpu.cfs_quota_us"));
5679
+ const periodUs = numberIn(file(dirs.cpu, "cpu.cfs_period_us"));
5680
+ const throttledNs = numberIn(file(dirs.cpu, "cpu.stat"), /^throttled_time (\d+)/m);
5681
+ return {
5682
+ version: 1,
5683
+ memoryLimitBytes: limit !== null && limit < UNLIMITED ? limit : null,
5684
+ memoryCurrentBytes: numberIn(file(dirs.memory, "memory.usage_in_bytes")),
5685
+ memoryPeakBytes: numberIn(file(dirs.memory, "memory.max_usage_in_bytes")),
5686
+ oomKills: numberIn(file(dirs.memory, "memory.oom_control"), /^oom_kill (\d+)/m),
5687
+ cpuQuotaCores: quotaUs !== null && quotaUs > 0 && periodUs ? quotaUs / periodUs : null,
5688
+ throttledMs: throttledNs === null ? null : throttledNs / 1e6
5689
+ };
5690
+ }
5691
+ };
5692
+
5693
+ // src/internal/capture/worker-metrics.ts
5694
+ var MB = 1024 * 1024;
5695
+ function round1(value) {
5696
+ return Math.round(value * 10) / 10;
5697
+ }
5698
+ var WorkerMetrics = class {
5699
+ constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
5700
+ this.proc = proc;
5701
+ this.pid = pid;
5702
+ this.loop = null;
5703
+ this.usage = null;
5704
+ /** Each process's CPU and wait at the start of the current window; null before the first. */
5705
+ this.subtree = null;
5706
+ this.roles = /* @__PURE__ */ new Map();
5707
+ this.linux = platform === "linux" && proc.available();
5708
+ let histogram = null;
5709
+ try {
5710
+ histogram = (0, import_node_perf_hooks.monitorEventLoopDelay)({ resolution: 20 });
5711
+ histogram.enable();
5712
+ } catch {
5713
+ histogram = null;
5714
+ }
5715
+ this.histogram = histogram;
5716
+ }
5717
+ /** The worker's processes under it, keyed by pid and start time. */
5718
+ tree() {
5719
+ const table = this.proc.table();
5720
+ return descendantsOf(this.pid, table.values()).map((stat) => {
5721
+ const key = `${stat.pid}:${stat.start}`;
5722
+ let role = this.roles.get(key);
5723
+ if (!role) {
5724
+ role = roleOf(stat.comm, this.proc.cmdline(stat.pid)) ?? "other";
5725
+ this.roles.set(key, role);
5726
+ }
5727
+ return { key, pid: stat.pid, cpuMs: stat.cpuMs, role };
5728
+ });
5729
+ }
5730
+ /**
5731
+ * The processes under the worker now, and the reset of their peak memory: the
5732
+ * start of the next window. Taken once per test, at its end, so the time
5733
+ * between two tests counts toward the next one.
5734
+ */
5735
+ snapshot() {
5736
+ this.subtree = /* @__PURE__ */ new Map();
5737
+ for (const proc of this.tree()) {
5738
+ this.subtree.set(proc.key, { cpuMs: proc.cpuMs, waitMs: this.proc.runWaitMs(proc.pid) });
5739
+ this.proc.resetPeakRss(proc.pid);
5740
+ }
5741
+ }
5742
+ /**
5743
+ * Start a test's window. `processes` says whether the worker has started any
5744
+ * browser to read; a worker that never did skips the process tree.
5745
+ */
5746
+ start(processes = true) {
5747
+ try {
5748
+ this.histogram?.reset();
5749
+ this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
5750
+ this.usage = process.resourceUsage();
5751
+ if (this.linux && processes && !this.subtree) this.snapshot();
5752
+ } catch {
5753
+ }
5754
+ }
5755
+ /** Close the window. Null when the test's start was never read. */
5756
+ end(processes = true) {
5757
+ if (!this.usage || !this.loop) return null;
5758
+ try {
5759
+ const usage = process.resourceUsage();
5760
+ const loop = import_node_perf_hooks.performance.eventLoopUtilization(this.loop);
5761
+ const metrics = {
5762
+ worker: {
5763
+ cpuMs: Math.round(
5764
+ (usage.userCPUTime - this.usage.userCPUTime + usage.systemCPUTime - this.usage.systemCPUTime) / 1e3
5765
+ ),
5766
+ involuntarySwitches: usage.involuntaryContextSwitches - this.usage.involuntaryContextSwitches,
5767
+ loopUtilization: Math.round(loop.utilization * 1e3) / 1e3,
5768
+ loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
5769
+ loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
5770
+ heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
5771
+ fds: this.linux ? this.proc.fdCount(this.pid) : null
5772
+ }
5773
+ };
5774
+ this.usage = null;
5775
+ this.loop = null;
5776
+ if (this.linux && processes) metrics.roles = this.roleCosts();
5777
+ return metrics;
5778
+ } catch {
5779
+ return null;
5780
+ }
5781
+ }
5782
+ /** The window's cost by role; what it reads becomes the start of the next window. */
5783
+ roleCosts() {
5784
+ const roles = {};
5785
+ const start = this.subtree ?? /* @__PURE__ */ new Map();
5786
+ const next = /* @__PURE__ */ new Map();
5787
+ for (const proc of this.tree()) {
5788
+ const from = start.get(proc.key) ?? { cpuMs: 0, waitMs: 0 };
5789
+ const waitMs = this.proc.runWaitMs(proc.pid);
5790
+ const peakKb = this.proc.statusKb(proc.pid, "VmHWM");
5791
+ const cost = roles[proc.role] ??= { cpuMs: 0, runWaitMs: null, peakRssMb: null, processes: 0 };
5792
+ cost.cpuMs += Math.max(0, proc.cpuMs - from.cpuMs);
5793
+ if (waitMs !== null && from.waitMs !== null)
5794
+ cost.runWaitMs = round1((cost.runWaitMs ?? 0) + Math.max(0, waitMs - from.waitMs));
5795
+ if (peakKb !== null) cost.peakRssMb = Math.max(cost.peakRssMb ?? 0, round1(peakKb / 1024));
5796
+ cost.processes++;
5797
+ next.set(proc.key, { cpuMs: proc.cpuMs, waitMs });
5798
+ this.proc.resetPeakRss(proc.pid);
5799
+ }
5800
+ this.subtree = next;
5801
+ return roles;
5802
+ }
5803
+ dispose() {
5804
+ this.histogram?.disable();
5805
+ }
5806
+ };
5807
+ var PageReader = class {
5808
+ constructor() {
5809
+ this.sessions = /* @__PURE__ */ new WeakMap();
5810
+ }
5811
+ async read(page) {
5812
+ let session = this.sessions.get(page);
5813
+ if (!session) {
5814
+ session = internalCall(page, async () => {
5815
+ const opened2 = await page.context().newCDPSession(page);
5816
+ await opened2.send("Performance.enable", { timeDomain: "threadTicks" });
5817
+ return opened2;
5818
+ }).catch(() => null);
5819
+ this.sessions.set(page, session);
5820
+ }
5821
+ const opened = await session;
5822
+ if (!opened) return null;
5823
+ try {
5824
+ const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
5825
+ const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
5826
+ return {
5827
+ cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
5828
+ heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
5829
+ nodes: values.get("Nodes") ?? 0,
5830
+ listeners: values.get("JSEventListeners") ?? 0
5831
+ };
5832
+ } catch {
5833
+ return null;
5834
+ }
5835
+ }
5836
+ };
5837
+
5838
+ // src/internal/capture/resource-ledger.ts
5839
+ var TRACKED_HANDLE_TYPES = ["TCPServerWrap", "FSEventWrap", "StatWatcher"];
5840
+ var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
5841
+ var CLOSE_TIMEOUT_MS = 5e3;
5842
+ var PAGE_READ_CAP = 50;
5843
+ var PAGE_READ_TIMEOUT_MS = 1e3;
5844
+ var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
5845
+ var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
5846
+ function relativePath(file) {
5847
+ return path20.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path20.sep).join("/");
5848
+ }
5849
+ function isLibraryFile(file) {
5850
+ return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path20.sep}node_modules${path20.sep}`) || file.includes("/node_modules/") || file === __filename;
5851
+ }
5852
+ function userSite(stack) {
5853
+ for (const line of (stack ?? "").split("\n").slice(1)) {
5854
+ const match = /^\s*at\s+(?:.*?\s+\()?(.+?):(\d+):\d+\)?$/.exec(line);
5855
+ if (!match) continue;
5856
+ const file = match[1].replace(/^file:\/\//, "");
5857
+ if (isLibraryFile(file) || !/\.[a-z]+$/i.test(file)) continue;
5858
+ return `${relativePath(file)}:${match[2]}`;
5859
+ }
5860
+ return null;
5861
+ }
5862
+ function zoneSite(zone) {
5863
+ for (const frame of zone?.frames ?? []) {
5864
+ if (!frame?.file || isLibraryFile(frame.file)) continue;
5865
+ return `${relativePath(frame.file)}:${frame.line ?? 0}`;
5866
+ }
5867
+ return null;
5868
+ }
5869
+ function testRef2(info) {
5870
+ if (!info?.testId) return null;
5871
+ const titlePath = Array.isArray(info.titlePath) ? info.titlePath : [];
5872
+ return {
5873
+ id: info.testId,
5874
+ file: info.file ? relativePath(info.file) : "",
5875
+ suite: titlePath.slice(1, -1).map(String)
5876
+ };
5877
+ }
5878
+ function runnableOf(info) {
5879
+ if (!info) return { phase: "worker", fixture: null };
5880
+ const runnable = info._timeoutManager?._running?.runnable;
5881
+ const type = typeof runnable?.type === "string" ? runnable.type : "test";
5882
+ const phase = HOOK_PHASES.has(type) ? type : type === "teardown" ? "worker" : "test";
5883
+ const raw = runnable?.fixture;
5884
+ const fixture = raw && typeof raw.title === "string" ? {
5885
+ title: raw.title,
5886
+ location: raw.location?.file ? `${relativePath(raw.location.file)}:${raw.location.line ?? 0}` : null,
5887
+ worker: raw.slot !== void 0 && raw.slot !== null
5888
+ } : null;
5889
+ return { phase, fixture };
5890
+ }
5891
+ function fallbackRunnable(window) {
5892
+ return { phase: window ? "test" : "worker", fixture: null };
5893
+ }
5894
+ function playwrightTestInfoSource() {
5895
+ try {
5896
+ const packageJson = resolvePlaywrightPackageJson();
5897
+ if (!packageJson) return () => null;
5898
+ const globals = (0, import_node_module2.createRequire)(packageJson)(path20.join(path20.dirname(packageJson), "lib", "globals.js"));
5899
+ const read = globals.currentTestInfo;
5900
+ if (typeof read !== "function") return () => null;
5901
+ return () => {
5902
+ try {
5903
+ return read() ?? null;
5904
+ } catch {
5905
+ return null;
5906
+ }
5907
+ };
5908
+ } catch {
5909
+ return () => null;
5910
+ }
5911
+ }
5912
+ function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
5913
+ return value === "fail" || value === "close" ? value : "report";
5914
+ }
5915
+ function countHandles() {
5916
+ const counts = {};
5917
+ const read = process.getActiveResourcesInfo;
5918
+ if (typeof read !== "function") return counts;
5919
+ for (const type of read.call(process)) {
5920
+ if (TRACKED_HANDLE_TYPES.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
5921
+ }
5922
+ return counts;
5923
+ }
5924
+ async function settledHandles(start) {
5925
+ const end = countHandles();
5926
+ if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
5927
+ await new Promise((resolve7) => setTimeout(resolve7, 0));
5928
+ return countHandles();
5929
+ }
5930
+ function settleWithin(promise, ms) {
5931
+ let timer;
5932
+ return Promise.race([
5933
+ promise.catch(() => void 0),
5934
+ new Promise((resolve7) => {
5935
+ timer = setTimeout(resolve7, ms);
5936
+ })
5937
+ ]).finally(() => clearTimeout(timer));
5938
+ }
5939
+ var ResourceLedger = class {
5940
+ constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}) {
5941
+ this.playwright = playwright;
5942
+ this.workerIndex = workerIndex;
5943
+ this.leakCheck = leakCheck;
5944
+ this.resultsFile = resultsFile;
5945
+ this.testInfoSource = testInfoSource;
5946
+ this.now = now;
5947
+ this.readers = readers;
5948
+ this.nextId = 1;
5949
+ this.entries = /* @__PURE__ */ new Map();
5950
+ this.byObject = /* @__PURE__ */ new WeakMap();
5951
+ this.pendingCalls = /* @__PURE__ */ new Set();
5952
+ this.lastCall = null;
5953
+ /** The page a wrapped page method is calling into: its first reported API call marks it used. */
5954
+ this.usingPage = null;
5955
+ this.born = [];
5956
+ this.closed = [];
5957
+ this.handlesAtStart = {};
5958
+ this.openAtStart = { contexts: 0, pages: 0 };
5959
+ /** The test between `testStarted` and `testEnded`, for when Playwright's own test info cannot be read. */
5960
+ this.window = null;
5961
+ this.listener = null;
5962
+ /** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
5963
+ this.sawBrowser = false;
5964
+ }
5965
+ /** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
5966
+ install() {
5967
+ const instrumentation = this.playwright?._instrumentation;
5968
+ if (!instrumentation || typeof instrumentation.addListener !== "function") return false;
5969
+ this.listener = {
5970
+ onApiCallBegin: (zone) => {
5971
+ if (this.pendingCalls.size > 1e3) this.pendingCalls.clear();
5972
+ this.pendingCalls.add(zone);
5973
+ this.lastCall = zone;
5974
+ if (this.usingPage) {
5975
+ this.usingPage.used = true;
5976
+ this.usingPage = null;
5977
+ }
5978
+ },
5979
+ onApiCallEnd: (zone) => {
5980
+ this.pendingCalls.delete(zone);
5981
+ },
5982
+ runAfterCreateBrowserContext: async (context) => {
5983
+ this.guard(() => this.onContext(context, userSite(new Error().stack)));
5984
+ },
5985
+ runAfterCreateRequestContext: async (request) => {
5986
+ this.guard(() => this.track(request, "request", null, userSite(new Error().stack)));
5987
+ },
5988
+ runBeforeCloseRequestContext: async (request) => {
5989
+ this.guard(() => this.onClosed(request));
5990
+ }
5991
+ };
5992
+ instrumentation.addListener(this.listener);
5993
+ for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
5994
+ if (type) this.wrapBrowserType(type);
5995
+ }
5996
+ return true;
5997
+ }
5998
+ /** Stop listening; the objects already tracked stay in the ledger. */
5999
+ uninstall() {
6000
+ const remove = this.playwright?._instrumentation?.removeListener;
6001
+ if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
6002
+ this.listener = null;
6003
+ }
6004
+ /** Mark a page used: a locator was built on it. */
6005
+ notePageUse(page) {
6006
+ const entry = this.byObject.get(page);
6007
+ if (entry) entry.used = true;
6008
+ }
6009
+ /** Open the census window of a test. */
6010
+ testStarted(info) {
6011
+ this.window = testRef2(info);
6012
+ this.handlesAtStart = countHandles();
6013
+ this.openAtStart = { contexts: 0, pages: 0 };
6014
+ for (const entry of this.openEntries()) {
6015
+ if (entry.birth.kind === "page") this.openAtStart.pages++;
6016
+ else if (entry.birth.kind === "context") this.openAtStart.contexts++;
6017
+ }
6018
+ this.readers.metrics?.start(this.sawBrowser);
6019
+ }
6020
+ /**
6021
+ * Close the census window of a test: apply `PIWI_LEAK_CHECK=close` to what the
6022
+ * test left open, then take the census. Called after the test-scoped fixtures
6023
+ * tore down, so what the test opened through them is closed by now.
6024
+ */
6025
+ async testEnded(info) {
6026
+ const test = testRef2(info) ?? this.window;
6027
+ const failed = typeof info.status === "string" && typeof info.expectedStatus === "string" && info.status !== info.expectedStatus;
6028
+ const leaks = test && !failed ? this.leaksOf(test.id) : [];
6029
+ if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
6030
+ const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
6031
+ const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
6032
+ const handles = { start: this.handlesAtStart, end: await settledHandles(this.handlesAtStart) };
6033
+ const census = this.census(ref, handles, await this.readPages());
6034
+ if (metrics) census.metrics = metrics;
6035
+ if (test) {
6036
+ census.openAtStart = this.openAtStart;
6037
+ if (!failed) census.leftOpen = leaks.length;
6038
+ }
6039
+ this.window = null;
6040
+ return { census, leaks };
6041
+ }
6042
+ /** The open pages' main thread and weight, over CDP, within a deadline. */
6043
+ async readPages() {
6044
+ const out = /* @__PURE__ */ new Map();
6045
+ const reader = this.readers.pages;
6046
+ if (!reader) return out;
6047
+ const pages = this.openEntries().filter((entry) => entry.birth.kind === "page").slice(0, PAGE_READ_CAP);
6048
+ await settleWithin(
6049
+ Promise.all(
6050
+ pages.map(async (entry) => {
6051
+ const main = entry.object ? await reader.read(entry.object) : null;
6052
+ if (main) out.set(entry.birth.id, main);
6053
+ })
6054
+ ),
6055
+ PAGE_READ_TIMEOUT_MS
6056
+ );
6057
+ return out;
6058
+ }
6059
+ /** The census a worker takes as it shuts down, appended to the results file when the reporter set one. */
6060
+ workerEnded() {
6061
+ const census = this.census(null);
6062
+ if (this.resultsFile) {
6063
+ try {
6064
+ fs19.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
6065
+ `);
6066
+ } catch {
6067
+ }
6068
+ }
6069
+ this.readers.metrics?.dispose();
6070
+ this.uninstall();
6071
+ return census;
6072
+ }
6073
+ guard(run) {
6074
+ try {
6075
+ run();
6076
+ } catch {
6077
+ }
6078
+ }
6079
+ wrapBrowserType(type) {
6080
+ for (const method of ["launch", "connect", "connectOverCDP"]) {
6081
+ const original = type[method];
6082
+ if (typeof original !== "function") continue;
6083
+ const bound = original.bind(type);
6084
+ type[method] = async (...args) => {
6085
+ const site = userSite(new Error().stack);
6086
+ const browser = await bound(...args);
6087
+ this.guard(() => this.onBrowser(browser, site));
6088
+ return browser;
6089
+ };
6090
+ }
6091
+ }
6092
+ track(object, kind, parent, site) {
6093
+ const existing = this.byObject.get(object);
6094
+ if (existing) return existing;
6095
+ const info = this.testInfoSource();
6096
+ const { phase, fixture } = info ? runnableOf(info) : fallbackRunnable(this.window);
6097
+ const birth = {
6098
+ id: this.nextId++,
6099
+ kind,
6100
+ parent: parent ? this.byObject.get(parent)?.birth.id ?? null : null,
6101
+ at: this.now(),
6102
+ phase,
6103
+ fixture,
6104
+ test: testRef2(info) ?? this.window,
6105
+ site
6106
+ };
6107
+ const entry = { birth, object, closed: null, used: false };
6108
+ if (kind === "browser" || kind === "context") this.sawBrowser = true;
6109
+ this.entries.set(birth.id, entry);
6110
+ this.byObject.set(object, entry);
6111
+ this.born.push(birth);
6112
+ return entry;
6113
+ }
6114
+ onBrowser(browser, site) {
6115
+ const known = this.byObject.get(browser);
6116
+ if (known) return known;
6117
+ const entry = this.track(browser, "browser", null, site);
6118
+ browser.on("disconnected", () => this.guard(() => this.onClosed(browser)));
6119
+ return entry;
6120
+ }
6121
+ onContext(context, site) {
6122
+ if (this.byObject.has(context)) return;
6123
+ const browser = context.browser();
6124
+ if (browser) this.onBrowser(browser, site);
6125
+ const entry = this.track(context, "context", browser, site);
6126
+ const zone = this.latestPendingCall();
6127
+ if (zone?.apiName?.endsWith(".newPage")) entry.birth.implicit = true;
6128
+ if (context._forReuse) entry.birth.reused = true;
6129
+ context.on("close", () => this.guard(() => this.onClosed(context)));
6130
+ context.on("page", (page) => this.guard(() => this.onPage(page, context)));
6131
+ for (const page of context.pages()) this.onPage(page, context, { site });
6132
+ }
6133
+ /** `initial` is set for a page that came with its context (a persistent context's), which is no popup. */
6134
+ onPage(page, context, initial) {
6135
+ if (this.byObject.has(page)) return;
6136
+ const zone = this.latestPendingCall();
6137
+ const viaNewPage = !!zone?.apiName?.endsWith(".newPage");
6138
+ const popup = !viaNewPage && !initial;
6139
+ const site = viaNewPage ? zoneSite(zone) : initial ? initial.site : zoneSite(this.lastCall);
6140
+ const entry = this.track(page, "page", context, site);
6141
+ if (popup) {
6142
+ const opener = page._opener;
6143
+ const openerEntry = opener ? this.byObject.get(opener) : void 0;
6144
+ if (openerEntry) entry.birth.opener = openerEntry.birth.id;
6145
+ if (this.lastCall?.apiName) entry.birth.trigger = this.lastCall.apiName;
6146
+ }
6147
+ const markUsed = () => {
6148
+ entry.used = true;
6149
+ };
6150
+ page.on("load", markUsed);
6151
+ page.on("domcontentloaded", markUsed);
6152
+ page.on("framenavigated", (frame) => {
6153
+ try {
6154
+ if (frame === page.mainFrame() && frame.url() !== "about:blank") entry.used = true;
6155
+ } catch {
6156
+ }
6157
+ });
6158
+ page.on("close", () => this.guard(() => this.onClosed(page)));
6159
+ const methods = page;
6160
+ for (const method of USE_METHODS) {
6161
+ const original = methods[method];
6162
+ if (typeof original !== "function") continue;
6163
+ const bound = original.bind(page);
6164
+ methods[method] = (...args) => {
6165
+ const previous = this.usingPage;
6166
+ this.usingPage = entry;
6167
+ try {
6168
+ return bound(...args);
6169
+ } finally {
6170
+ this.usingPage = previous;
6171
+ }
6172
+ };
6173
+ }
6174
+ }
6175
+ onClosed(object, byPiwi = false) {
6176
+ const entry = this.byObject.get(object);
6177
+ if (!entry || entry.closed) return;
6178
+ const info = this.testInfoSource();
6179
+ const { phase } = info ? runnableOf(info) : fallbackRunnable(this.window);
6180
+ const close = {
6181
+ id: entry.birth.id,
6182
+ at: this.now(),
6183
+ phase,
6184
+ test: testRef2(info) ?? this.window
6185
+ };
6186
+ if (entry.birth.kind === "page") close.used = entry.used;
6187
+ if (byPiwi) close.byPiwi = true;
6188
+ entry.closed = close;
6189
+ entry.object = null;
6190
+ this.closed.push(close);
6191
+ }
6192
+ latestPendingCall() {
6193
+ let latest = null;
6194
+ for (const zone of this.pendingCalls) latest = zone;
6195
+ return latest;
6196
+ }
6197
+ openEntries() {
6198
+ return [...this.entries.values()].filter((entry) => !entry.closed && entry.object);
6199
+ }
6200
+ /**
6201
+ * Objects a test opened itself, by its body or its each-hooks, and left open.
6202
+ * A page in a leaked context, or a context in a leaked browser, goes with it;
6203
+ * the context `browser.newPage()` opened for its page goes with the page.
6204
+ */
6205
+ leaksOf(testId) {
6206
+ const open = this.openEntries().filter(
6207
+ (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
6208
+ );
6209
+ const openIds = new Set(open.map((entry) => entry.birth.id));
6210
+ const foldsIntoLeak = (entry) => {
6211
+ let parentId = entry.birth.parent;
6212
+ while (parentId !== null) {
6213
+ if (!this.isImplicit(parentId)) return openIds.has(parentId);
6214
+ parentId = this.entries.get(parentId)?.birth.parent ?? null;
6215
+ }
6216
+ return false;
6217
+ };
6218
+ return open.filter((entry) => !(entry.birth.kind === "context" && this.isImplicit(entry.birth.id))).filter((entry) => !foldsIntoLeak(entry)).map((entry) => ({
6219
+ id: entry.birth.id,
6220
+ kind: entry.birth.kind,
6221
+ site: entry.birth.site,
6222
+ pages: entry.birth.kind === "page" ? 0 : this.pagesUnder(entry.birth.id),
6223
+ implicit: false,
6224
+ ...entry.birth.opener !== void 0 ? { trigger: entry.birth.trigger ?? "a click" } : {}
6225
+ })).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
6226
+ }
6227
+ /** Open pages under a context, or under any context of a browser. */
6228
+ pagesUnder(id) {
6229
+ let count = 0;
6230
+ for (const entry of this.openEntries()) {
6231
+ if (entry.birth.kind !== "page" || entry.birth.parent === null) continue;
6232
+ const context = this.entries.get(entry.birth.parent);
6233
+ if (entry.birth.parent === id || context?.birth.parent === id) count++;
6234
+ }
6235
+ return count;
6236
+ }
6237
+ isImplicit(id) {
6238
+ const entry = this.entries.get(id);
6239
+ if (!entry || entry.birth.kind !== "context") return false;
6240
+ if (entry.birth.implicit) return true;
6241
+ const owner = entry.object ? entry.object._ownerPage : void 0;
6242
+ if (owner) entry.birth.implicit = true;
6243
+ return !!owner;
6244
+ }
6245
+ openPagesOf(contextId) {
6246
+ let count = 0;
6247
+ for (const entry of this.openEntries())
6248
+ if (entry.birth.kind === "page" && entry.birth.parent === contextId) count++;
6249
+ return count;
6250
+ }
6251
+ async closeLeaks(leaks) {
6252
+ for (const leak of leaks) {
6253
+ const entry = this.entries.get(leak.id);
6254
+ const object = entry?.object;
6255
+ if (!entry || entry.closed || !object) continue;
6256
+ const close = leak.kind === "request" ? () => object.dispose() : () => object.close();
6257
+ await settleWithin(internalCall(object, close), CLOSE_TIMEOUT_MS);
6258
+ const closedMeanwhile = entry.closed;
6259
+ if (closedMeanwhile) closedMeanwhile.byPiwi = true;
6260
+ else this.onClosed(object, true);
6261
+ }
6262
+ }
6263
+ census(test, handles, mains = /* @__PURE__ */ new Map()) {
6264
+ const open = [];
6265
+ for (const entry of this.openEntries()) {
6266
+ const object = entry.object;
6267
+ if (entry.birth.kind === "page") {
6268
+ const main = mains.get(entry.birth.id);
6269
+ open.push({
6270
+ id: entry.birth.id,
6271
+ used: entry.used,
6272
+ url: this.pageUrl(object),
6273
+ listeners: this.listenerCount(object),
6274
+ routes: this.routeCount(object),
6275
+ ...main ? { main } : {}
6276
+ });
6277
+ } else if (entry.birth.kind === "context") {
6278
+ this.isImplicit(entry.birth.id);
6279
+ open.push({ id: entry.birth.id, pages: this.openPagesOf(entry.birth.id), routes: this.routeCount(object) });
6280
+ } else {
6281
+ open.push({ id: entry.birth.id });
6282
+ }
6283
+ }
6284
+ const census = {
6285
+ v: 1,
6286
+ worker: this.workerIndex,
6287
+ pid: process.pid,
6288
+ at: this.now(),
6289
+ test,
6290
+ born: this.born,
6291
+ closed: this.closed,
6292
+ open
6293
+ };
6294
+ if (handles) census.handles = handles;
6295
+ this.born = [];
6296
+ this.closed = [];
6297
+ return census;
6298
+ }
6299
+ pageUrl(page) {
6300
+ try {
6301
+ const url = typeof page.url === "function" ? String(page.url()) : "";
6302
+ return url ? url.split(/[?#]/)[0] : void 0;
6303
+ } catch {
6304
+ return void 0;
6305
+ }
6306
+ }
6307
+ listenerCount(emitter) {
6308
+ try {
6309
+ if (typeof emitter.eventNames !== "function" || typeof emitter.listenerCount !== "function") return void 0;
6310
+ const names = emitter.eventNames();
6311
+ return names.reduce((sum, name) => sum + emitter.listenerCount(name), 0);
6312
+ } catch {
6313
+ return void 0;
6314
+ }
6315
+ }
6316
+ routeCount(object) {
6317
+ const routes = object._routes;
6318
+ return Array.isArray(routes) ? routes.length : void 0;
6319
+ }
6320
+ };
6321
+ var activeLedger = null;
6322
+ function startResourceLedger(playwright, workerIndex) {
6323
+ if (process.env[PIWI_ENV_KEYS.captureResources] === "false") return null;
6324
+ if (activeLedger) return activeLedger;
6325
+ const metrics = new WorkerMetrics();
6326
+ const ledger = new ResourceLedger(
6327
+ playwright,
6328
+ workerIndex,
6329
+ readLeakCheck(),
6330
+ process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
6331
+ playwrightTestInfoSource(),
6332
+ Date.now,
6333
+ { metrics, pages: new PageReader() }
6334
+ );
6335
+ if (!ledger.install()) {
6336
+ metrics.dispose();
6337
+ return null;
6338
+ }
6339
+ activeLedger = ledger;
6340
+ return ledger;
6341
+ }
6342
+ function activeResourceLedger() {
6343
+ return activeLedger;
6344
+ }
6345
+ function stopResourceLedger() {
6346
+ const ledger = activeLedger;
6347
+ activeLedger = null;
6348
+ return ledger ? ledger.workerEnded() : null;
6349
+ }
6350
+ function noteResourceUse(page) {
6351
+ activeLedger?.notePageUse(page);
6352
+ }
6353
+ var ARTICLE = {
6354
+ browser: "a browser",
6355
+ context: "a context",
6356
+ page: "a page",
6357
+ request: "an API request context"
6358
+ };
6359
+ function leakFailureMessage(leaks) {
6360
+ const lines = leaks.map((leak) => {
6361
+ const what = leak.trigger ? `a popup opened by ${leak.trigger}` : ARTICLE[leak.kind];
6362
+ const where = leak.site ? `${leak.trigger ? "" : " opened"} at ${leak.site}` : "";
6363
+ const pages = leak.pages > 0 ? `, with ${leak.pages} page${leak.pages === 1 ? "" : "s"}` : "";
6364
+ return ` - ${what}${where}${pages}`;
6365
+ });
6366
+ return [
6367
+ `Piwi: this test left ${leaks.length === 1 ? "one object" : `${leaks.length} objects`} open:`,
6368
+ ...lines,
6369
+ "Close each before the test ends (await using, close() or dispose()), or set PIWI_LEAK_CHECK=report to only list them."
6370
+ ].join("\n");
6371
+ }
6372
+ async function recordResourceCensus(testInfo) {
6373
+ const ledger = activeLedger;
6374
+ if (!ledger) return;
6375
+ const { census, leaks } = await ledger.testEnded(testInfo);
6376
+ try {
6377
+ await testInfo.attach(ATTACHMENT_NAMES.resources, {
6378
+ contentType: "application/json",
6379
+ body: Buffer.from(JSON.stringify(census))
6380
+ });
6381
+ } catch {
6382
+ }
6383
+ if (ledger.leakCheck === "fail" && leaks.length > 0) throw new Error(leakFailureMessage(leaks));
6384
+ }
6385
+
6386
+ // src/internal/collect/resource-verdicts.ts
6387
+ var path21 = __toESM(require("path"));
6388
+ var OPEN_STEP_TITLES = {
6389
+ "Create context": "context",
6390
+ "Launch persistent context": "context",
6391
+ "Launch browser": "browser",
6392
+ "Connect over CDP": "browser"
6393
+ };
6394
+ var CLOSE_STEP_TITLES = {
6395
+ "Close context": "context",
6396
+ "Close browser": "browser"
6397
+ };
6398
+ function parseResourceCensus(body) {
6399
+ if (!body) return null;
6400
+ try {
6401
+ const value = JSON.parse(body.toString());
6402
+ if (value?.v !== 1 || !Array.isArray(value.born) || !Array.isArray(value.closed) || !Array.isArray(value.open)) {
6403
+ return null;
6404
+ }
6405
+ if (typeof value.worker !== "number" || typeof value.at !== "number") return null;
6406
+ return value;
6407
+ } catch {
6408
+ return null;
6409
+ }
6410
+ }
6411
+ function parseResourceResults(text) {
6412
+ return text.split("\n").map((line) => line.trim() ? parseResourceCensus(line) : null).filter((census) => census !== null);
6413
+ }
6414
+ function userFixturesOf(steps) {
6415
+ const out = [];
6416
+ const walk = (list) => {
6417
+ for (const step of list) {
6418
+ const file = step.location?.file;
6419
+ if (step.category === "fixture" && file && !file.includes(`${path21.sep}node_modules${path21.sep}`)) {
6420
+ const name = step.title.replace(/^(?:Fixture\s+|fixture:\s*)/, "").replace(/^"(.*)"$/, "$1");
6421
+ if (!name.startsWith("piwi")) out.push(`${name} (${relativeFile(file)}:${step.location.line})`);
6422
+ }
6423
+ walk(step.steps ?? []);
6424
+ }
6425
+ };
6426
+ walk(steps);
6427
+ return [...new Set(out)];
6428
+ }
6429
+ function tallyLifecycleSteps(file, steps) {
6430
+ const tally = { file, opened: [], closed: { context: 0, browser: 0 } };
6431
+ const walk = (list) => {
6432
+ for (const step of list) {
6433
+ if (step.category === "fixture") continue;
6434
+ if (step.category === "pw:api") {
6435
+ const opened = OPEN_STEP_TITLES[step.title];
6436
+ const closed = CLOSE_STEP_TITLES[step.title];
6437
+ if (opened) {
6438
+ tally.opened.push({
6439
+ kind: opened,
6440
+ site: step.location ? `${relativeFile(step.location.file)}:${step.location.line}` : null
6441
+ });
6442
+ }
6443
+ if (closed) tally.closed[closed]++;
6444
+ }
6445
+ walk(step.steps ?? []);
6446
+ }
6447
+ };
6448
+ walk(steps);
6449
+ return tally;
6450
+ }
6451
+ function relativeFile(file) {
6452
+ return path21.relative(process.cwd(), file).split(path21.sep).join("/");
6453
+ }
6454
+ function buildResourceReport(input) {
6455
+ const findings = [];
6456
+ const byWorker = /* @__PURE__ */ new Map();
6457
+ for (const census of input.censuses) {
6458
+ const list = byWorker.get(census.worker) ?? [];
6459
+ list.push(census);
6460
+ byWorker.set(census.worker, list);
6461
+ }
6462
+ const leaked = [];
6463
+ const idle = [];
6464
+ const all = [];
6465
+ for (const censuses of byWorker.values()) {
6466
+ censuses.sort((a, b) => a.at - b.at);
6467
+ const tracked = /* @__PURE__ */ new Map();
6468
+ for (const census of censuses) {
6469
+ for (const birth of census.born) {
6470
+ tracked.set(birth.id, {
6471
+ worker: census.worker,
6472
+ birth,
6473
+ bornCensus: census,
6474
+ close: null,
6475
+ lastOpenAt: null,
6476
+ used: void 0,
6477
+ series: [],
6478
+ mainCpu: []
6479
+ });
6480
+ }
6481
+ for (const close of census.closed) {
6482
+ const entry = tracked.get(close.id);
6483
+ if (!entry) continue;
6484
+ entry.close = close;
6485
+ if (close.used !== void 0) entry.used = close.used;
6486
+ }
6487
+ for (const open of census.open) {
6488
+ const entry = tracked.get(open.id);
6489
+ if (!entry) continue;
6490
+ entry.lastOpenAt = census.at;
6491
+ if (open.used !== void 0) entry.used = open.used;
6492
+ if (census.test) entry.series.push({ pages: open.pages, listeners: open.listeners, routes: open.routes });
6493
+ if (open.main) entry.mainCpu.push({ at: census.at, cpuMs: open.main.cpuMs });
6494
+ }
6495
+ }
6496
+ const testCensuses = censuses.filter((census) => census.test !== null);
6497
+ const workerEnded = censuses.some((census) => census.test === null);
6498
+ for (const entry of tracked.values()) {
6499
+ all.push(entry);
6500
+ if (entry.birth.kind === "page" && entry.used === false && !entry.bornCensus.test?.failed) idle.push(entry);
6501
+ const verdict = leakVerdict(entry, testCensuses, workerEnded);
6502
+ if (verdict) leaked.push({ tracked: entry, ...verdict });
6503
+ }
6504
+ }
6505
+ findings.push(...groupLeaks(leaked, all));
6506
+ findings.push(...pilingFindings(all));
6507
+ findings.push(...handleFindings(byWorker));
6508
+ findings.push(...groupIdle(idle, input.fixturesByTest ?? /* @__PURE__ */ new Map()));
6509
+ findings.push(...probableFindings(input.tallies ?? []));
6510
+ const counts = { leaked: 0, idle: 0, piling: 0, handle: 0, probable: 0 };
6511
+ for (const finding of findings) counts[finding.verdict] += finding.verdict === "idle" ? finding.count : 1;
6512
+ return { findings, counts };
6513
+ }
6514
+ function sameFile(birth, ref) {
6515
+ return !!ref && !!birth.test && ref.file === birth.test.file;
6516
+ }
6517
+ function insideBlock(birth, ref) {
6518
+ if (!ref || !sameFile(birth, ref)) return false;
6519
+ return birth.test.suite.every((title, index) => ref.suite[index] === title);
6520
+ }
6521
+ function leakVerdict(entry, testCensuses, workerEnded) {
6522
+ const { birth, close, bornCensus } = entry;
6523
+ if (birth.reused || birth.phase === "worker" || birth.fixture?.worker || !birth.test) return null;
6524
+ if (birth.kind === "context" && birth.implicit) return null;
6525
+ if (close?.byPiwi) return { scope: "test", heldMs: 0, untilWorkerEnd: false };
6526
+ const closedByShutdown = !!close && (close.phase === "worker" || close.test === null);
6527
+ if (birth.phase === "beforeAll" || birth.phase === "afterAll") {
6528
+ if (close && !closedByShutdown && sameFile(birth, close.test)) return null;
6529
+ const ended = workerEnded || testCensuses.some((census) => census.at >= bornCensus.at && !insideBlock(birth, census.test));
6530
+ if (!ended) return null;
6531
+ const lastInside = [...testCensuses].reverse().find((census) => insideBlock(birth, census.test));
6532
+ const blockEnd = lastInside?.at ?? bornCensus.at;
6533
+ const end2 = close?.at ?? entry.lastOpenAt ?? blockEnd;
6534
+ return { scope: "describe", heldMs: Math.max(0, end2 - blockEnd), untilWorkerEnd: !close || closedByShutdown };
6535
+ }
6536
+ if (bornCensus.test === null || bornCensus.test.id !== birth.test.id) return null;
6537
+ if (bornCensus.test.failed) return null;
6538
+ const openAtItsEnd = bornCensus.open.some((open) => open.id === birth.id);
6539
+ if (!openAtItsEnd) return null;
6540
+ if (close && !closedByShutdown && (close.test?.id === birth.test.id || sameFile(birth, close.test))) return null;
6541
+ const end = close?.at ?? entry.lastOpenAt ?? bornCensus.at;
6542
+ return { scope: "test", heldMs: Math.max(0, end - bornCensus.at), untilWorkerEnd: !close || closedByShutdown };
6543
+ }
6544
+ function whereOf(birth) {
6545
+ if (birth.opener !== void 0) {
6546
+ return `popup${birth.trigger ? ` after ${birth.trigger}` : ""}${birth.site ? ` at ${birth.site}` : ""}`;
6547
+ }
6548
+ if (birth.fixture) {
6549
+ const at = birth.site ?? birth.fixture.location;
6550
+ return `fixture "${birth.fixture.title}"${at ? ` at ${at}` : ""}`;
6551
+ }
6552
+ return birth.site ?? "an unknown line";
6553
+ }
6554
+ function afterTestCpu(entry) {
6555
+ const readings = entry.mainCpu;
6556
+ if (readings.length < 2) return 0;
6557
+ const first = readings.find((reading) => reading.at >= entry.bornCensus.at) ?? readings[0];
6558
+ return Math.max(0, readings[readings.length - 1].cpuMs - first.cpuMs);
6559
+ }
6560
+ function siteOf(birth) {
6561
+ return birth.site ?? birth.fixture?.location ?? null;
6562
+ }
6563
+ var keyOf = (worker, id) => `${worker}:${id}`;
6564
+ function groupLeaks(leaked, all) {
6565
+ const leaks = new Map(leaked.map((leak) => [keyOf(leak.tracked.worker, leak.tracked.birth.id), leak]));
6566
+ const tracked = new Map(all.map((entry) => [keyOf(entry.worker, entry.birth.id), entry]));
6567
+ const groups = /* @__PURE__ */ new Map();
6568
+ const groupKey = (leak) => `${leak.scope}|${leak.tracked.birth.kind}|${whereOf(leak.tracked.birth)}`;
6569
+ for (const leak of leaked) {
6570
+ const { birth } = leak.tracked;
6571
+ if (birth.kind === "context" && birth.implicit) continue;
6572
+ if (foldTarget(leak.tracked, leaks, tracked)) continue;
6573
+ const key = groupKey(leak);
6574
+ const group = groups.get(key) ?? {
6575
+ verdict: "leaked",
6576
+ kind: birth.kind,
6577
+ where: whereOf(birth),
6578
+ site: siteOf(birth),
6579
+ scope: leak.scope,
6580
+ tests: 0,
6581
+ count: 0,
6582
+ heldMs: 0,
6583
+ untilWorkerEnd: false,
6584
+ pages: 0,
6585
+ testIds: /* @__PURE__ */ new Set(),
6586
+ ...leak.scope === "describe" && birth.test?.suite.length ? { detail: `beforeAll of "${birth.test.suite.join(" \u203A ")}"` } : {}
6587
+ };
6588
+ group.count++;
6589
+ if (birth.test) group.testIds.add(birth.test.id);
6590
+ group.heldMs = Math.max(group.heldMs ?? 0, leak.heldMs);
6591
+ group.untilWorkerEnd = group.untilWorkerEnd || leak.untilWorkerEnd;
6592
+ if (leak.tracked.close?.byPiwi) group.closedByPiwi = true;
6593
+ if (birth.kind === "page" && leak.scope === "test") addCpu(group, afterTestCpu(leak.tracked));
6594
+ groups.set(key, group);
6595
+ }
6596
+ for (const leak of leaked) {
6597
+ if (leak.tracked.birth.kind !== "page") continue;
6598
+ const root = foldTarget(leak.tracked, leaks, tracked);
6599
+ const group = root ? groups.get(groupKey(root)) : void 0;
6600
+ if (!group) continue;
6601
+ group.pages = (group.pages ?? 0) + 1;
6602
+ if (root.scope === "test") addCpu(group, afterTestCpu(leak.tracked));
6603
+ }
6604
+ 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);
6605
+ }
6606
+ function addCpu(group, cpuMs) {
6607
+ if (cpuMs > 0) group.afterTestCpuMs = (group.afterTestCpuMs ?? 0) + cpuMs;
6608
+ }
6609
+ function foldTarget(entry, leaks, tracked) {
6610
+ let parentId = entry.birth.parent;
6611
+ while (parentId !== null) {
6612
+ const key = keyOf(entry.worker, parentId);
6613
+ const parent = tracked.get(key);
6614
+ if (!parent) return null;
6615
+ if (!(parent.birth.kind === "context" && parent.birth.implicit)) return leaks.get(key) ?? null;
6616
+ parentId = parent.birth.parent;
6617
+ }
6618
+ return null;
6619
+ }
6620
+ function groupIdle(idle, fixturesByTest) {
6621
+ const groups = /* @__PURE__ */ new Map();
6622
+ for (const entry of idle) {
6623
+ const { birth } = entry;
6624
+ const fixtures = birth.test ? fixturesByTest.get(birth.test.id) ?? [] : [];
6625
+ const detail = birth.fixture && fixtures.length ? `set up with ${fixtures.join(", ")}` : void 0;
6626
+ const where = whereOf(birth);
6627
+ const key = `${where}|${detail ?? ""}`;
6628
+ const group = groups.get(key) ?? {
6629
+ verdict: "idle",
6630
+ kind: "page",
6631
+ where,
6632
+ site: siteOf(birth),
6633
+ tests: 0,
6634
+ count: 0,
6635
+ testIds: /* @__PURE__ */ new Set(),
6636
+ ...detail ? { detail } : {}
6637
+ };
6638
+ group.count++;
6639
+ if (birth.test) group.testIds.add(birth.test.id);
6640
+ groups.set(key, group);
6641
+ }
6642
+ return [...groups.values()].map(({ testIds, ...finding }) => ({ ...finding, tests: testIds.size })).sort((a, b) => b.count - a.count);
6643
+ }
6644
+ function pilingFindings(all) {
6645
+ const out = [];
6646
+ for (const entry of all) {
6647
+ for (const what of ["pages", "listeners", "routes"]) {
6648
+ const values = entry.series.map((point) => point[what]).filter((v) => typeof v === "number");
6649
+ if (values.length < 3) continue;
6650
+ const rising = values.every((value, i) => i === 0 || value >= values[i - 1]);
6651
+ const from = values[0];
6652
+ const to = values[values.length - 1];
6653
+ if (!rising || to - from < 2) continue;
6654
+ out.push({
6655
+ verdict: "piling",
6656
+ kind: entry.birth.kind,
6657
+ where: whereOf(entry.birth),
6658
+ site: siteOf(entry.birth),
6659
+ tests: values.length,
6660
+ count: 1,
6661
+ growth: { what, from, to, tests: values.length }
6662
+ });
6663
+ }
6664
+ }
6665
+ return out.sort((a, b) => b.growth.to - b.growth.from - (a.growth.to - a.growth.from));
6666
+ }
6667
+ function handleFindings(byWorker) {
6668
+ const groups = /* @__PURE__ */ new Map();
6669
+ for (const censuses of byWorker.values()) {
6670
+ const ordered = censuses.filter((census) => census.test && census.handles).sort((a, b) => a.at - b.at);
6671
+ for (let i = 0; i < ordered.length - 1; i++) {
6672
+ const census = ordered[i];
6673
+ const next = ordered[i + 1];
6674
+ for (const [type, end] of Object.entries(census.handles.end)) {
6675
+ const grew = end - (census.handles.start[type] ?? 0);
6676
+ if (grew <= 0 || (next.handles.start[type] ?? 0) < end) continue;
6677
+ const test = census.test;
6678
+ const key = `${test.id}|${type}`;
6679
+ const group = groups.get(key) ?? {
6680
+ verdict: "handle",
6681
+ kind: "handle",
6682
+ where: type,
6683
+ tests: 1,
6684
+ count: 0,
6685
+ detail: `"${test.title ?? ""}" (${test.file})`
6686
+ };
6687
+ group.count += grew;
6688
+ groups.set(key, group);
6689
+ }
6690
+ }
6691
+ }
6692
+ return [...groups.values()];
6693
+ }
6694
+ function probableFindings(tallies) {
6695
+ const byFile = /* @__PURE__ */ new Map();
6696
+ for (const tally of tallies) {
6697
+ const merged = byFile.get(tally.file) ?? { file: tally.file, opened: [], closed: { context: 0, browser: 0 } };
6698
+ merged.opened.push(...tally.opened);
6699
+ merged.closed.context += tally.closed.context;
6700
+ merged.closed.browser += tally.closed.browser;
6701
+ byFile.set(tally.file, merged);
6702
+ }
6703
+ const out = [];
6704
+ for (const tally of byFile.values()) {
6705
+ for (const kind of ["browser", "context"]) {
6706
+ const opened = tally.opened.filter((o) => o.kind === kind);
6707
+ if (opened.length <= tally.closed[kind]) continue;
6708
+ const sites = [...new Set(opened.map((o) => o.site ?? tally.file))];
6709
+ out.push({
6710
+ verdict: "probable",
6711
+ kind,
6712
+ where: sites.join(", "),
6713
+ tests: 0,
6714
+ count: opened.length - tally.closed[kind],
6715
+ detail: `${opened.length} opened, ${tally.closed[kind]} closed in ${tally.file}`
6716
+ });
6717
+ }
6718
+ }
6719
+ return out;
6720
+ }
6721
+ function formatHeld(ms) {
6722
+ if (ms < 1e3) return `${Math.round(ms)} ms`;
6723
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)} s`;
6724
+ const minutes = Math.floor(ms / 6e4);
6725
+ const seconds = Math.round(ms % 6e4 / 1e3);
6726
+ return seconds ? `${minutes} min ${seconds} s` : `${minutes} min`;
6727
+ }
6728
+ var KIND_LABEL = {
6729
+ browser: "browser",
6730
+ context: "context",
6731
+ page: "page",
6732
+ request: "API context",
6733
+ handle: "handle"
6734
+ };
6735
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
6736
+ var HANDLE_LABEL = {
6737
+ TCPServerWrap: "server",
6738
+ FSEventWrap: "file watcher",
6739
+ StatWatcher: "file watcher"
6740
+ };
6741
+ function findingLine(finding) {
6742
+ const verdict = finding.verdict.padEnd(8);
6743
+ const label = finding.verdict === "handle" ? HANDLE_LABEL[finding.where] ?? finding.where : finding.growth ? finding.growth.what : KIND_LABEL[finding.kind];
6744
+ const kind = label.padEnd(11);
6745
+ const parts = finding.verdict === "handle" ? [] : [finding.where];
6746
+ switch (finding.verdict) {
6747
+ case "leaked": {
6748
+ if (finding.count > 1) parts.push(`${finding.count} ${KIND_LABEL[finding.kind]}s`);
6749
+ if (finding.detail) parts.push(finding.detail);
6750
+ if (finding.pages) parts.push(`with ${plural(finding.pages, "page")}`);
6751
+ if (finding.tests) parts.push(plural(finding.tests, "test"));
6752
+ const past = finding.scope === "describe" ? "its describe block" : "its test";
6753
+ if (finding.closedByPiwi) parts.push("closed by Piwi at the end of its test");
6754
+ else if (finding.untilWorkerEnd)
6755
+ parts.push(`open until the worker shut down (${formatHeld(finding.heldMs ?? 0)} past ${past})`);
6756
+ else parts.push(`open ${formatHeld(finding.heldMs ?? 0)} past ${past}`);
6757
+ if ((finding.afterTestCpuMs ?? 0) >= 500)
6758
+ parts.push(`${formatHeld(finding.afterTestCpuMs)} of page CPU after its test`);
6759
+ break;
6760
+ }
6761
+ case "idle":
6762
+ parts.push(plural(finding.count, "page"));
6763
+ if (finding.tests) parts.push(plural(finding.tests, "test"));
6764
+ if (finding.detail) parts.push(finding.detail);
6765
+ break;
6766
+ case "piling": {
6767
+ const growth = finding.growth;
6768
+ const noun = growth.what === "pages" ? "open pages" : growth.what === "routes" ? "route handlers" : "listeners";
6769
+ parts.push(`${growth.from} \u2192 ${growth.to} ${noun} over ${growth.tests} tests`);
6770
+ break;
6771
+ }
6772
+ case "handle":
6773
+ parts.push(`left running in the worker by ${finding.detail ?? "a test"}`);
6774
+ break;
6775
+ case "probable":
6776
+ if (finding.detail) parts.push(finding.detail);
6777
+ break;
6778
+ }
6779
+ return ` ${verdict} ${kind} ${parts.join(" \xB7 ")}`;
6780
+ }
6781
+ function formatResourceSummary(report, leakCheck, maxLines = 10) {
6782
+ if (report.findings.length === 0) return [];
6783
+ const { counts } = report;
6784
+ const head = [
6785
+ counts.leaked ? plural(counts.leaked, "leak") : "",
6786
+ counts.idle ? `${plural(counts.idle, "idle page")}` : "",
6787
+ counts.piling ? `${counts.piling} piling up` : "",
6788
+ counts.handle ? plural(counts.handle, "handle") + " left in workers" : "",
6789
+ counts.probable ? plural(counts.probable, "probable leak") : ""
6790
+ ].filter(Boolean);
6791
+ const lines = [`Resources: ${head.join(" \xB7 ")}`];
6792
+ for (const finding of report.findings.slice(0, maxLines)) lines.push(findingLine(finding));
6793
+ const rest = report.findings.length - maxLines;
6794
+ if (rest > 0) lines.push(` \u2026 and ${plural(rest, "more finding")}`);
6795
+ const testLeaks = report.findings.some((f) => f.verdict === "leaked" && f.scope === "test" && !f.closedByPiwi);
6796
+ if (testLeaks && leakCheck === "report") {
6797
+ lines.push(
6798
+ "Set leakCheck: 'fail' (PIWI_LEAK_CHECK=fail) to fail the tests that leave what they open, or 'close' to close it."
6799
+ );
6800
+ }
6801
+ if (counts.probable) {
6802
+ lines.push(
6803
+ "Probable leaks are counted from the steps alone; the capture fixtures (extendPiwiFixtures) make them exact."
6804
+ );
6805
+ }
6806
+ return lines;
6807
+ }
6808
+
6809
+ // src/internal/collect/process-sampler.ts
6810
+ var fs20 = __toESM(require("fs"));
6811
+ var os8 = __toESM(require("os"));
6812
+ var path22 = __toESM(require("path"));
6813
+ var import_node_child_process2 = require("child_process");
6814
+ var WALK_FILE_CAP = 1e4;
6815
+ var SERIES_CAP = 3600;
6816
+ var PLAYWRIGHT_TEMP = /^(playwright_\w+dev_profile-|playwright-artifacts-)/;
6817
+ function round12(value) {
6818
+ return Math.round(value * 10) / 10;
6819
+ }
6820
+ function pct(part, whole) {
6821
+ return whole > 0 ? round12(part / whole * 100) : null;
6822
+ }
6823
+ function nodeCpuTicks() {
6824
+ const cpus2 = os8.cpus();
6825
+ if (cpus2.length === 0) return null;
6826
+ let busy = 0;
6827
+ let idle = 0;
6828
+ for (const cpu of cpus2) {
6829
+ busy += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq;
6830
+ idle += cpu.times.idle;
6831
+ }
6832
+ return { busy, idle, iowait: 0, steal: 0, total: busy + idle };
6833
+ }
6834
+ function defaultPs() {
6835
+ return new Promise((resolve7, reject) => {
6836
+ (0, import_node_child_process2.execFile)(
6837
+ "ps",
6838
+ ["-A", "-o", "pid=,ppid=,rss=,time=,command="],
6839
+ { timeout: 2e3, maxBuffer: 8 * 1024 * 1024 },
6840
+ (error, stdout) => error ? reject(error) : resolve7(stdout)
6841
+ );
6842
+ });
6843
+ }
6844
+ function freeBytes(dir) {
6845
+ let current = path22.resolve(dir);
6846
+ for (; ; ) {
6847
+ try {
6848
+ const stats = fs20.statfsSync(current);
6849
+ return stats.bavail * stats.bsize;
6850
+ } catch {
6851
+ const parent = path22.dirname(current);
6852
+ if (parent === current) return null;
6853
+ current = parent;
6854
+ }
6855
+ }
6856
+ }
6857
+ async function dirBytes(dir, budget) {
6858
+ let bytes = 0;
6859
+ let entries;
6860
+ try {
6861
+ entries = await fs20.promises.readdir(dir, { withFileTypes: true });
6862
+ } catch {
6863
+ return 0;
6864
+ }
6865
+ for (const entry of entries) {
6866
+ if (budget.files <= 0) return bytes;
6867
+ const full = path22.join(dir, entry.name);
6868
+ if (entry.isDirectory()) bytes += await dirBytes(full, budget);
6869
+ else if (entry.isFile()) {
6870
+ budget.files--;
6871
+ try {
6872
+ bytes += (await fs20.promises.lstat(full)).size;
6873
+ } catch {
6874
+ }
6875
+ }
6876
+ }
6877
+ return bytes;
6878
+ }
6879
+ var RunSampler = class {
6880
+ constructor(options = {}) {
6881
+ this.timer = null;
6882
+ this.running = null;
6883
+ this.samples = 0;
6884
+ this.startedAt = 0;
6885
+ this.tracked = /* @__PURE__ */ new Map();
6886
+ this.treeMeasured = false;
6887
+ this.waitMeasured = false;
6888
+ this.cpuStart = null;
6889
+ this.cpuPrevious = null;
6890
+ this.series = [];
6891
+ this.pressureStart = { cpu: null, memory: null };
6892
+ this.containerStart = null;
6893
+ this.memoryKind = null;
6894
+ this.peakBytes = null;
6895
+ this.peakAtMs = null;
6896
+ this.largest = null;
6897
+ this.rssFallbackPids = /* @__PURE__ */ new Set();
6898
+ this.lowestAvailableKb = null;
6899
+ this.peakInUse = null;
6900
+ this.peakInUseIsLowerBound = false;
6901
+ this.lowestFree = null;
6902
+ this.rootPid = options.rootPid ?? process.pid;
6903
+ this.platform = options.platform ?? process.platform;
6904
+ this.proc = new ProcFs(options.procRoot);
6905
+ this.cgroup = new CgroupFs(options.procRoot, options.cgroupRoot);
6906
+ this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path22.resolve(dir)))];
6907
+ this.tmpDir = options.tmpDir ?? os8.tmpdir();
6908
+ this.intervalMs = options.intervalMs ?? 1e3;
6909
+ this.heavyEvery = Math.max(1, options.heavyEvery ?? 5);
6910
+ this.now = options.now ?? Date.now;
6911
+ this.runPs = options.runPs ?? defaultPs;
6912
+ this.linux = this.platform === "linux" && this.proc.available();
6913
+ }
6914
+ /** Take the first sample and sample on a timer that never keeps the process alive. */
6915
+ start() {
6916
+ if (this.timer) return;
6917
+ this.startedAt = this.now();
6918
+ this.cpuStart = this.cpuTicks();
6919
+ this.cpuPrevious = this.cpuStart;
6920
+ if (this.linux) {
6921
+ this.pressureStart = { cpu: this.proc.pressureUs("cpu"), memory: this.proc.pressureUs("memory") };
6922
+ this.containerStart = this.cgroup.read();
6923
+ }
6924
+ this.schedule();
6925
+ this.timer = setInterval(() => this.schedule(), this.intervalMs);
6926
+ this.timer.unref?.();
6927
+ }
6928
+ /** Take the last sample and return the run's profile. Never throws. */
6929
+ async stop() {
6930
+ if (this.timer) clearInterval(this.timer);
6931
+ this.timer = null;
6932
+ try {
6933
+ await this.running;
6934
+ await this.sample(true);
6935
+ } catch {
6936
+ }
6937
+ return this.profile(await this.leftoverBytes());
6938
+ }
6939
+ schedule() {
6940
+ if (this.running) return;
6941
+ const heavy = this.samples % this.heavyEvery === 0;
6942
+ this.running = this.sample(heavy).catch(() => void 0).finally(() => {
6943
+ this.running = null;
6944
+ });
6945
+ }
6946
+ cpuTicks() {
6947
+ return this.linux ? this.proc.cpuTicks() : nodeCpuTicks();
6948
+ }
6949
+ async sample(heavy) {
6950
+ this.samples++;
6951
+ const ticks = this.cpuTicks();
6952
+ if (ticks && this.cpuPrevious) {
6953
+ const busy = pct(ticks.busy - this.cpuPrevious.busy, ticks.total - this.cpuPrevious.total);
6954
+ if (busy !== null && this.series.length < SERIES_CAP) this.series.push(busy);
6955
+ }
6956
+ if (ticks) this.cpuPrevious = ticks;
6957
+ const availableKb = this.linux ? this.proc.memAvailableKb() : Math.round(os8.freemem() / 1024);
6958
+ if (availableKb !== null) this.lowestAvailableKb = Math.min(this.lowestAvailableKb ?? Infinity, availableKb);
6959
+ if (this.linux) await this.sampleLinuxTree(heavy);
6960
+ else if (this.platform === "darwin" && heavy) await this.samplePsTree();
6961
+ if (heavy) await this.sampleDisk();
6962
+ }
6963
+ track(key, role, cpuMs, waitMs) {
6964
+ const known = this.tracked.get(key);
6965
+ if (known) {
6966
+ known.lastCpuMs = cpuMs;
6967
+ known.lastWaitMs = waitMs;
6968
+ if (role) known.role = role;
6969
+ return;
6970
+ }
6971
+ const first = this.samples === 1;
6972
+ this.tracked.set(key, {
6973
+ role,
6974
+ firstCpuMs: first ? cpuMs : 0,
6975
+ firstWaitMs: first ? waitMs : waitMs === null ? null : 0,
6976
+ lastCpuMs: cpuMs,
6977
+ lastWaitMs: waitMs
6978
+ });
6979
+ }
6980
+ /** The role of each process of the tree, a child of the runner being the web server unless it says otherwise. */
6981
+ resolveRoles(tree, explicit) {
6982
+ const roles = /* @__PURE__ */ new Map([[this.rootPid, "runner"]]);
6983
+ for (const proc of tree) {
6984
+ const parent = roles.get(proc.ppid);
6985
+ const inherited = parent === "runner" || parent === "webServer" ? "webServer" : "other";
6986
+ roles.set(proc.pid, explicit(proc) ?? inherited);
6987
+ }
6988
+ return roles;
6989
+ }
6990
+ async sampleLinuxTree(heavy) {
6991
+ const table = this.proc.table();
6992
+ const root = table.get(this.rootPid);
6993
+ if (!root) return;
6994
+ this.treeMeasured = true;
6995
+ const tree = [root, ...descendantsOf(this.rootPid, table.values())];
6996
+ const roles = this.resolveRoles(tree.slice(1), (proc) => {
6997
+ const key = `${proc.pid}:${proc.start}`;
6998
+ const known = this.tracked.get(key)?.role;
6999
+ return known && !heavy ? known : roleOf(proc.comm, this.proc.cmdline(proc.pid));
7000
+ });
7001
+ for (const proc of tree) {
7002
+ const waitMs = this.proc.runWaitMs(proc.pid);
7003
+ if (waitMs !== null) this.waitMeasured = true;
7004
+ this.track(`${proc.pid}:${proc.start}`, roles.get(proc.pid) ?? "other", proc.cpuMs, waitMs);
7005
+ }
7006
+ if (!heavy) return;
7007
+ let total = 0;
7008
+ let largest = null;
7009
+ const memory = await Promise.all(
7010
+ tree.map(async (proc) => {
7011
+ const pss = await this.proc.pssKb(proc.pid);
7012
+ if (pss !== null) return { proc, kb: pss, fallback: false };
7013
+ return { proc, kb: this.proc.statusKb(proc.pid, "VmRSS"), fallback: true };
7014
+ })
7015
+ );
7016
+ for (const { proc, kb, fallback } of memory) {
7017
+ if (kb === null) continue;
7018
+ if (fallback) this.rssFallbackPids.add(`${proc.pid}:${proc.start}`);
7019
+ total += kb * 1024;
7020
+ if (!largest || kb * 1024 > largest.bytes) largest = { role: roles.get(proc.pid) ?? "other", bytes: kb * 1024 };
7021
+ }
7022
+ this.memoryKind = memory.some((m) => m.kb !== null && !m.fallback) ? "pss" : memory.length ? "rss" : null;
7023
+ this.notePeak(total, largest);
7024
+ }
7025
+ async samplePsTree() {
7026
+ let list;
7027
+ try {
7028
+ list = parsePsOutput(await this.runPs());
7029
+ } catch {
7030
+ return;
7031
+ }
7032
+ const root = list.find((proc) => proc.pid === this.rootPid);
7033
+ if (!root) return;
7034
+ this.treeMeasured = true;
7035
+ const tree = [root, ...descendantsOf(this.rootPid, list)];
7036
+ const roles = this.resolveRoles(
7037
+ tree.slice(1),
7038
+ (proc) => roleOf(path22.basename(proc.command.split(" ")[0] ?? ""), proc.command)
7039
+ );
7040
+ let total = 0;
7041
+ let largest = null;
7042
+ for (const proc of tree) {
7043
+ const role = roles.get(proc.pid) ?? "other";
7044
+ this.track(String(proc.pid), role, proc.cpuMs, null);
7045
+ total += proc.rssKb * 1024;
7046
+ if (!largest || proc.rssKb * 1024 > largest.bytes) largest = { role, bytes: proc.rssKb * 1024 };
7047
+ }
7048
+ this.memoryKind = "rss";
7049
+ this.notePeak(total, largest);
7050
+ }
7051
+ notePeak(total, largest) {
7052
+ if (this.peakBytes === null || total > this.peakBytes) {
7053
+ this.peakBytes = total;
7054
+ this.peakAtMs = this.now() - this.startedAt;
7055
+ }
7056
+ if (largest && (!this.largest || largest.bytes > this.largest.bytes)) this.largest = largest;
7057
+ }
7058
+ async sampleDisk() {
7059
+ for (const dir of [...this.outputDirs, this.tmpDir]) {
7060
+ const free = freeBytes(dir);
7061
+ if (free !== null) this.lowestFree = Math.min(this.lowestFree ?? Infinity, free);
7062
+ }
7063
+ const budget = { files: WALK_FILE_CAP };
7064
+ let bytes = 0;
7065
+ for (const dir of this.outputDirs) bytes += await dirBytes(dir, budget);
7066
+ for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs >= this.startedAt - 1e3)) {
7067
+ bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
7068
+ }
7069
+ if (budget.files <= 0) this.peakInUseIsLowerBound = true;
7070
+ this.peakInUse = Math.max(this.peakInUse ?? 0, bytes);
7071
+ }
7072
+ /** Playwright's folders in the temp directory whose modification time passes `keep`. */
7073
+ async playwrightTempEntries(keep) {
7074
+ let names;
7075
+ try {
7076
+ names = await fs20.promises.readdir(this.tmpDir);
7077
+ } catch {
7078
+ return [];
7079
+ }
7080
+ const out = [];
7081
+ for (const name of names) {
7082
+ if (!PLAYWRIGHT_TEMP.test(name)) continue;
7083
+ try {
7084
+ if (keep((await fs20.promises.stat(path22.join(this.tmpDir, name))).mtimeMs)) out.push(name);
7085
+ } catch {
7086
+ }
7087
+ }
7088
+ return out;
7089
+ }
7090
+ async leftoverBytes() {
7091
+ try {
7092
+ const budget = { files: WALK_FILE_CAP };
7093
+ let bytes = 0;
7094
+ for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs < this.startedAt - 1e3)) {
7095
+ bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
7096
+ }
7097
+ return bytes;
7098
+ } catch {
7099
+ return null;
7100
+ }
7101
+ }
7102
+ profile(leftoverBytes) {
7103
+ const wallMs = Math.max(0, this.now() - this.startedAt);
7104
+ const notMeasured = [];
7105
+ let byRole = null;
7106
+ if (this.treeMeasured) {
7107
+ byRole = {};
7108
+ for (const entry of this.tracked.values()) {
7109
+ const role = entry.role ?? "other";
7110
+ const usage = byRole[role] ??= { cpuMs: 0, runWaitMs: this.waitMeasured ? 0 : null, processes: 0 };
7111
+ usage.cpuMs += Math.max(0, entry.lastCpuMs - entry.firstCpuMs);
7112
+ if (usage.runWaitMs !== null && entry.lastWaitMs !== null && entry.firstWaitMs !== null) {
7113
+ usage.runWaitMs += Math.max(0, entry.lastWaitMs - entry.firstWaitMs);
7114
+ }
7115
+ usage.processes++;
7116
+ }
7117
+ } else {
7118
+ notMeasured.push("CPU and memory of the run\u2019s processes");
7119
+ }
7120
+ if (this.treeMeasured && !this.waitMeasured) notMeasured.push("time waiting for a CPU");
7121
+ const end = this.cpuTicks();
7122
+ const start = this.cpuStart;
7123
+ const total = end && start ? end.total - start.total : 0;
7124
+ const machineCpu = {
7125
+ busyPct: end && start ? pct(end.busy - start.busy, total) : null,
7126
+ iowaitPct: this.linux && end && start ? pct(end.iowait - start.iowait, total) : null,
7127
+ stealPct: this.linux && end && start ? pct(end.steal - start.steal, total) : null
7128
+ };
7129
+ if (!this.linux) notMeasured.push("steal");
7130
+ const pressure = (resource, from) => {
7131
+ if (!this.linux || from === null) return null;
7132
+ const to = this.proc.pressureUs(resource);
7133
+ return to === null || wallMs === 0 ? null : Math.min(100, round12((to - from) / 1e3 / wallMs * 100));
7134
+ };
7135
+ const cpuPressure = pressure("cpu", this.pressureStart.cpu);
7136
+ const memoryPressure = pressure("memory", this.pressureStart.memory);
7137
+ if (cpuPressure === null) notMeasured.push("CPU pressure");
7138
+ const container = this.linux ? this.cgroup.read() : null;
7139
+ const startContainer = this.containerStart;
7140
+ const totalMemory = os8.totalmem();
7141
+ const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
7142
+ const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
7143
+ const raisedPeak = container?.memoryPeakBytes != null && startContainer?.memoryPeakBytes != null && container.memoryPeakBytes > startContainer.memoryPeakBytes ? container.memoryPeakBytes : null;
7144
+ const delta = (to, from) => to != null && from != null ? Math.max(0, to - from) : null;
7145
+ return {
7146
+ platform: this.platform,
7147
+ wallMs,
7148
+ machine: {
7149
+ cores: typeof os8.availableParallelism === "function" ? os8.availableParallelism() : os8.cpus().length,
7150
+ memoryBytes: totalMemory,
7151
+ memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null,
7152
+ cpuQuotaCores: container?.cpuQuotaCores ?? null
7153
+ },
7154
+ cpu: {
7155
+ ...machineCpu,
7156
+ pressurePct: cpuPressure,
7157
+ series: this.series,
7158
+ byRole,
7159
+ throttledMs: delta(container?.throttledMs, startContainer?.throttledMs)
7160
+ },
7161
+ memory: {
7162
+ kind: this.memoryKind,
7163
+ peakBytes: this.peakBytes,
7164
+ peakAtMs: this.peakAtMs,
7165
+ largest: this.largest,
7166
+ rssFallbacks: this.memoryKind === "pss" ? this.rssFallbackPids.size : 0,
7167
+ pressurePct: memoryPressure,
7168
+ lowestAvailableBytes: this.lowestAvailableKb === null ? null : this.lowestAvailableKb * 1024,
7169
+ containerPeakBytes: raisedPeak,
7170
+ oomKills: delta(container?.oomKills, startContainer?.oomKills)
7171
+ },
7172
+ disk: {
7173
+ peakInUseBytes: this.peakInUse,
7174
+ peakInUseIsLowerBound: this.peakInUseIsLowerBound,
7175
+ lowestFreeBytes: this.lowestFree,
7176
+ leftoverBytes
7177
+ },
7178
+ notMeasured
7179
+ };
7180
+ }
7181
+ };
7182
+
7183
+ // src/internal/collect/machine-panel.ts
7184
+ function workerHealthOf(censuses) {
7185
+ const measured = censuses.filter((census) => census.test && census.metrics);
7186
+ if (measured.length === 0) return null;
7187
+ let utilization = 0;
7188
+ let switches = 0;
7189
+ let worstP99 = 0;
7190
+ for (const census of measured) {
7191
+ const worker = census.metrics.worker;
7192
+ utilization += worker.loopUtilization;
7193
+ switches += worker.involuntarySwitches;
7194
+ worstP99 = Math.max(worstP99, worker.loopDelayP99Ms);
7195
+ }
7196
+ return {
7197
+ tests: measured.length,
7198
+ loopUtilization: utilization / measured.length,
7199
+ loopDelayP99Ms: worstP99,
7200
+ involuntarySwitchesPerTest: switches / measured.length
7201
+ };
7202
+ }
7203
+ function artifactKind(name, contentType) {
7204
+ if (name === "trace") return "trace";
7205
+ if (name === "video" || contentType.startsWith("video/")) return "video";
7206
+ if (name === "screenshot" || contentType.startsWith("image/")) return "screenshot";
7207
+ return "other";
7208
+ }
7209
+ function emptyArtifacts() {
7210
+ return { trace: 0, video: 0, screenshot: 0, other: 0 };
7211
+ }
7212
+ var ROLE_LABEL = {
7213
+ renderer: "renderers",
7214
+ browser: "browsers",
7215
+ worker: "workers",
7216
+ gpu: "GPU",
7217
+ utility: "browser utilities",
7218
+ ffmpeg: "ffmpeg",
7219
+ webServer: "web server",
7220
+ runner: "runner",
7221
+ other: "other"
7222
+ };
7223
+ var ROLE_SINGULAR = {
7224
+ renderer: "renderer",
7225
+ browser: "browser",
7226
+ worker: "worker",
7227
+ gpu: "GPU",
7228
+ utility: "browser utility",
7229
+ ffmpeg: "ffmpeg",
7230
+ webServer: "web server",
7231
+ runner: "runner",
7232
+ other: "other"
7233
+ };
7234
+ var ARTIFACT_LABEL = {
7235
+ trace: "traces",
7236
+ video: "videos",
7237
+ screenshot: "screenshots",
7238
+ other: "other"
7239
+ };
7240
+ var BARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
7241
+ function formatBytes(bytes) {
7242
+ if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
7243
+ if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MB`;
7244
+ return `${Math.max(0, Math.round(bytes / 1024))} kB`;
7245
+ }
7246
+ function sparkline(series, width = 24) {
7247
+ if (series.length < 3) return "";
7248
+ const buckets = Math.min(width, series.length);
7249
+ let out = "";
7250
+ for (let i = 0; i < buckets; i++) {
7251
+ const slice = series.slice(
7252
+ Math.floor(i * series.length / buckets),
7253
+ Math.floor((i + 1) * series.length / buckets)
7254
+ );
7255
+ const mean = slice.reduce((sum, v) => sum + v, 0) / Math.max(1, slice.length);
7256
+ out += BARS[Math.min(BARS.length - 1, Math.max(0, Math.floor(mean / 100 * BARS.length)))];
7257
+ }
7258
+ return out;
7259
+ }
7260
+ function clock(ms) {
7261
+ const seconds = Math.round(ms / 1e3);
7262
+ return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
7263
+ }
7264
+ function cpuLine(profile) {
7265
+ const parts = [];
7266
+ const spark = sparkline(profile.cpu.series);
7267
+ if (profile.cpu.busyPct !== null) parts.push(`${spark ? `${spark} ` : ""}${Math.round(profile.cpu.busyPct)}% busy`);
7268
+ if (profile.cpu.pressurePct !== null) {
7269
+ parts.push(`a task waited for a CPU ${Math.round(profile.cpu.pressurePct)}% of the time`);
7270
+ }
7271
+ const roles = Object.entries(profile.cpu.byRole ?? {});
7272
+ const total = roles.reduce((sum, [, usage]) => sum + (usage?.cpuMs ?? 0), 0);
7273
+ if (total > 0) {
7274
+ 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)}`);
7275
+ parts.push(`${formatHeld(total)} of CPU: ${top.join(" \xB7 ")}`);
7276
+ const waiting = roles.filter(([, usage]) => (usage?.runWaitMs ?? 0) >= 1e3).sort((a, b) => (b[1]?.runWaitMs ?? 0) - (a[1]?.runWaitMs ?? 0))[0];
7277
+ if (waiting) parts.push(`${ROLE_LABEL[waiting[0]]} waited ${formatHeld(waiting[1].runWaitMs)} for a CPU`);
7278
+ }
7279
+ if (profile.cpu.throttledMs)
7280
+ parts.push(`the container's CPU quota held it back ${formatHeld(profile.cpu.throttledMs)}`);
7281
+ return parts.length ? ` CPU ${parts.join(" \xB7 ")}` : null;
7282
+ }
7283
+ function memoryLine(profile) {
7284
+ const { memory, machine } = profile;
7285
+ const parts = [];
7286
+ if (memory.kind && memory.peakBytes !== null) {
7287
+ const fallbacks = memory.rssFallbacks ? `, ${memory.rssFallbacks} by RSS` : "";
7288
+ const at = memory.peakAtMs !== null ? ` at ${clock(memory.peakAtMs)}` : "";
7289
+ parts.push(`peak ${formatBytes(memory.peakBytes)} (${memory.kind.toUpperCase()}${fallbacks})${at}`);
7290
+ const limit = machine.memoryLimitBytes;
7291
+ const whole = limit ?? machine.memoryBytes;
7292
+ if (whole > 0)
7293
+ parts.push(`${Math.round(memory.peakBytes / whole * 100)}% of the ${limit ? "limit" : "machine's memory"}`);
7294
+ if (memory.largest)
7295
+ parts.push(`largest process: ${ROLE_SINGULAR[memory.largest.role]} ${formatBytes(memory.largest.bytes)}`);
7296
+ }
7297
+ if (memory.containerPeakBytes !== null)
7298
+ parts.push(`the container peaked at ${formatBytes(memory.containerPeakBytes)}`);
7299
+ if (memory.oomKills) parts.push(`${memory.oomKills} process${memory.oomKills === 1 ? "" : "es"} killed for memory`);
7300
+ if (memory.pressurePct !== null) {
7301
+ parts.push(
7302
+ memory.pressurePct >= 0.1 ? `a task waited for memory ${memory.pressurePct}% of the time` : "no memory pressure"
7303
+ );
7304
+ }
7305
+ if (memory.lowestAvailableBytes !== null && memory.lowestAvailableBytes < machine.memoryBytes * 0.1) {
7306
+ parts.push(`${formatBytes(memory.lowestAvailableBytes)} available at the low point`);
7307
+ }
7308
+ return parts.length ? ` Memory ${parts.join(" \xB7 ")}` : null;
7309
+ }
7310
+ function diskLine(profile, artifacts) {
7311
+ const parts = [];
7312
+ if (artifacts) {
7313
+ const kinds = Object.entries(artifacts).filter(([, bytes]) => bytes > 0);
7314
+ const total = kinds.reduce((sum, [, bytes]) => sum + bytes, 0);
7315
+ parts.push(
7316
+ 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"
7317
+ );
7318
+ }
7319
+ const { disk } = profile;
7320
+ if (disk.peakInUseBytes !== null && disk.peakInUseBytes > 0) {
7321
+ parts.push(
7322
+ `${disk.peakInUseIsLowerBound ? "at least " : ""}${formatBytes(disk.peakInUseBytes)} in use at the peak`
7323
+ );
7324
+ }
7325
+ if (disk.lowestFreeBytes !== null) parts.push(`${formatBytes(disk.lowestFreeBytes)} free at the low point`);
7326
+ if (disk.leftoverBytes !== null && disk.leftoverBytes >= 10 * 1024 ** 2) {
7327
+ parts.push(`${formatBytes(disk.leftoverBytes)} left in the temp folder by earlier runs`);
7328
+ }
7329
+ return parts.length ? ` Disk ${parts.join(" \xB7 ")}` : null;
7330
+ }
7331
+ function workersLine(workers) {
7332
+ if (!workers || workers.tests === 0) return null;
7333
+ return ` Workers event loop busy ${Math.round(workers.loopUtilization * 100)}% \xB7 p99 delay ${Math.round(
7334
+ workers.loopDelayP99Ms
7335
+ )} ms at worst \xB7 ${Math.round(workers.involuntarySwitchesPerTest).toLocaleString("en-US")} involuntary context switches per test`;
7336
+ }
7337
+ function formatMachinePanel(profile, extras) {
7338
+ const { machine, cpu } = profile;
7339
+ const head = [
7340
+ `${machine.cores} cores${machine.cpuQuotaCores ? ` (a quota of ${machine.cpuQuotaCores})` : ""}`,
7341
+ formatBytes(machine.memoryBytes)
7342
+ ];
7343
+ if (machine.memoryLimitBytes) head.push(`container limit ${formatBytes(machine.memoryLimitBytes)}`);
7344
+ if (cpu.stealPct !== null && cpu.stealPct >= 1) head.push(`steal ${cpu.stealPct}%`);
7345
+ const lines = [
7346
+ `Machine: ${head.join(" \xB7 ")}`,
7347
+ cpuLine(profile),
7348
+ memoryLine(profile),
7349
+ diskLine(profile, extras.artifacts),
7350
+ workersLine(extras.workers)
7351
+ ].filter((line) => line !== null);
7352
+ if (profile.notMeasured.length) lines.push(` Not measured here: ${profile.notMeasured.join(", ")}`);
7353
+ return lines;
7354
+ }
7355
+
7356
+ // src/internal/collect/resource-wire.ts
7357
+ var REPORT_FINDINGS_CAP = 100;
7358
+ var REPORT_SERIES_CAP = 240;
7359
+ var REPORT_WORKER_POINTS_CAP = 200;
7360
+ function downsample(series, cap) {
7361
+ if (series.length <= cap) return series;
7362
+ const out = [];
7363
+ for (let i = 0; i < cap; i++) {
7364
+ const slice = series.slice(Math.floor(i * series.length / cap), Math.floor((i + 1) * series.length / cap));
7365
+ out.push(Math.round(slice.reduce((sum, v) => sum + v, 0) / Math.max(1, slice.length) * 10) / 10);
7366
+ }
7367
+ return out;
7368
+ }
7369
+ function nonZero(bytes) {
7370
+ const out = Object.fromEntries(Object.entries(bytes).filter(([, n]) => (n ?? 0) > 0));
7371
+ return Object.keys(out).length > 0 ? out : null;
7372
+ }
7373
+ function executionResources(census, artifacts) {
7374
+ const metrics = census.metrics;
7375
+ if (!metrics) return null;
7376
+ return {
7377
+ workerCpuMs: metrics.worker.cpuMs,
7378
+ roles: metrics.roles ?? null,
7379
+ loopUtilization: metrics.worker.loopUtilization,
7380
+ loopDelayP99Ms: metrics.worker.loopDelayP99Ms,
7381
+ involuntarySwitches: metrics.worker.involuntarySwitches,
7382
+ heapUsedMb: metrics.worker.heapUsedMb,
7383
+ openAtStart: census.openAtStart ?? null,
7384
+ leftOpen: census.leftOpen ?? null,
7385
+ artifactBytes: nonZero(artifacts)
7386
+ };
7387
+ }
7388
+ function openPagesByWorker(censuses) {
7389
+ const byWorker = /* @__PURE__ */ new Map();
7390
+ for (const census of censuses) {
7391
+ const list = byWorker.get(census.worker) ?? [];
7392
+ list.push(census);
7393
+ byWorker.set(census.worker, list);
7394
+ }
7395
+ const out = [];
7396
+ for (const [worker, list] of [...byWorker.entries()].sort((a, b) => a[0] - b[0])) {
7397
+ list.sort((a, b) => a.at - b.at);
7398
+ const kinds = /* @__PURE__ */ new Map();
7399
+ const points = [];
7400
+ for (const census of list) {
7401
+ for (const birth of census.born) kinds.set(birth.id, birth.kind);
7402
+ if (census.test) points.push(census.open.filter((open) => kinds.get(open.id) === "page").length);
7403
+ }
7404
+ if (points.length > 0) out.push({ worker, openPages: downsample(points, REPORT_WORKER_POINTS_CAP) });
7405
+ }
7406
+ return out;
7407
+ }
7408
+ function resourceReportWire(input) {
7409
+ const { report, profile } = input;
7410
+ return {
7411
+ v: 1,
7412
+ shardIndex: input.shardIndex,
7413
+ findings: (report?.findings ?? []).slice(0, REPORT_FINDINGS_CAP),
7414
+ counts: report?.counts ?? { leaked: 0, idle: 0, piling: 0, handle: 0, probable: 0 },
7415
+ profile: profile ? { ...profile, cpu: { ...profile.cpu, series: downsample(profile.cpu.series, REPORT_SERIES_CAP) } } : null,
7416
+ workers: openPagesByWorker(input.censuses),
7417
+ artifactBytes: nonZero(input.artifacts) ?? {},
7418
+ workerHealth: workerHealthOf(input.censuses)
7419
+ };
7420
+ }
7421
+
7422
+ // ../core/src/describe-failure.ts
7423
+ var HEADLINE_MAX_CHARS = 120;
7424
+ var VALUE_MAX_CHARS = 40;
7425
+ var SHORT_VALUE_MAX_CHARS = 20;
7426
+ var MASK_TOKEN_RE = /<(?:N|VALUE|URL|STR|UUID|HASH|EMAIL)>/g;
7427
+ var ACTION_VERBS = {
7428
+ click: "click",
7429
+ dblclick: "double-click",
7430
+ fill: "fill",
7431
+ type: "type",
7432
+ press: "press",
7433
+ pressSequentially: "type",
7434
+ check: "check",
7435
+ uncheck: "uncheck",
7436
+ hover: "hover",
7437
+ tap: "tap",
7438
+ focus: "focus",
7439
+ blur: "blur",
7440
+ clear: "clear",
7441
+ selectOption: "select",
7442
+ selectText: "select text",
7443
+ setInputFiles: "file upload",
7444
+ setChecked: "check",
7445
+ dragTo: "drag",
7446
+ dragAndDrop: "drag",
7447
+ scrollIntoViewIfNeeded: "scroll",
7448
+ screenshot: "screenshot",
7449
+ waitFor: "wait",
7450
+ waitForSelector: "waitForSelector",
7451
+ waitForLoadState: "waitForLoadState",
7452
+ waitForFunction: "waitForFunction",
7453
+ waitForResponse: "waitForResponse",
7454
+ waitForRequest: "waitForRequest",
7455
+ waitForEvent: "waitForEvent",
7456
+ waitForTimeout: "waitForTimeout",
7457
+ innerText: "read text",
7458
+ textContent: "read text",
7459
+ inputValue: "read value",
7460
+ getAttribute: "read attribute",
7461
+ isVisible: "visibility check",
7462
+ isEnabled: "enabled check",
7463
+ isChecked: "checked check",
7464
+ boundingBox: "measure",
7465
+ evaluate: "evaluate",
7466
+ goto: "navigation"
7467
+ };
7468
+ var ACTION_GERUNDS = {
7469
+ click: "clicking",
7470
+ dblclick: "double-clicking",
7471
+ fill: "filling",
7472
+ type: "typing into",
7473
+ press: "pressing a key on",
7474
+ pressSequentially: "typing into",
7475
+ check: "checking",
7476
+ uncheck: "unchecking",
7477
+ hover: "hovering",
7478
+ tap: "tapping",
7479
+ focus: "focusing",
7480
+ clear: "clearing",
7481
+ selectOption: "selecting an option in",
7482
+ setInputFiles: "uploading a file to",
7483
+ dragTo: "dragging",
7484
+ waitFor: "waiting for",
7485
+ waitForSelector: "waiting for",
7486
+ evaluate: "evaluating on"
7487
+ };
7488
+ var STATE_PHRASES = {
7489
+ "not-found": "was not found on the page",
7490
+ hidden: "never became visible",
7491
+ "not-visible": "never became visible",
7492
+ "not-enabled": "never became enabled",
7493
+ "not-editable": "never became editable",
7494
+ "not-stable": "never stopped moving",
7495
+ "outside-viewport": "stayed outside the viewport",
7496
+ "intercepts-pointer": "was covered by another element",
7497
+ detached: "was detached from the DOM",
7498
+ navigating: "was still navigating"
7499
+ };
7500
+ var STATE_MATCHERS = {
7501
+ toBeVisible: { met: "visible", unmet: "never became visible" },
7502
+ toBeHidden: { met: "hidden", unmet: "never became hidden" },
7503
+ toBeEnabled: { met: "enabled", unmet: "never became enabled" },
7504
+ toBeDisabled: { met: "disabled", unmet: "never became disabled" },
7505
+ toBeChecked: { met: "checked", unmet: "never became checked" },
7506
+ toBeEditable: { met: "editable", unmet: "never became editable" },
7507
+ toBeFocused: { met: "focused", unmet: "never received focus" },
7508
+ toBeAttached: { met: "attached", unmet: "never appeared in the DOM" },
7509
+ toBeDetached: { met: "detached", unmet: "never left the DOM" },
7510
+ toBeInViewport: { met: "in the viewport", unmet: "never entered the viewport" },
7511
+ toBeEmpty: { met: "empty", unmet: "never became empty" }
7512
+ };
7513
+ var VALUE_MATCHERS = {
7514
+ toHaveText: "text",
7515
+ toContainText: "text containing",
7516
+ toHaveValue: "value",
7517
+ toHaveValues: "values",
7518
+ toHaveAttribute: "attribute",
7519
+ toHaveClass: "class",
7520
+ toContainClass: "class",
7521
+ toHaveId: "id",
7522
+ toHaveCSS: "CSS",
7523
+ toHaveJSProperty: "property",
7524
+ toHaveAccessibleName: "accessible name",
7525
+ toHaveAccessibleDescription: "accessible description",
7526
+ toHaveRole: "role",
7527
+ toHaveURL: "URL",
7528
+ toHaveTitle: "title",
7529
+ toHaveScreenshot: "screenshot",
7530
+ toMatchAriaSnapshot: "ARIA snapshot"
7531
+ };
7532
+ var NETWORK_ERRORS = {
7533
+ ERR_CONNECTION_REFUSED: "Connection refused loading",
7534
+ ERR_CONNECTION_RESET: "Connection reset loading",
7535
+ ERR_CONNECTION_CLOSED: "Connection closed loading",
7536
+ ERR_CONNECTION_TIMED_OUT: "Connection timed out loading",
7537
+ ERR_TIMED_OUT: "Connection timed out loading",
7538
+ ERR_NAME_NOT_RESOLVED: "DNS lookup failed for",
7539
+ ERR_INTERNET_DISCONNECTED: "No network while loading",
7540
+ ERR_ADDRESS_UNREACHABLE: "Address unreachable loading",
7541
+ ERR_ABORTED: "Navigation aborted loading",
7542
+ ERR_EMPTY_RESPONSE: "Empty response loading",
7543
+ ERR_TOO_MANY_REDIRECTS: "Too many redirects loading",
7544
+ ERR_SSL_PROTOCOL_ERROR: "TLS error loading",
7545
+ ERR_CERT_AUTHORITY_INVALID: "Untrusted certificate loading",
7546
+ ERR_CERT_COMMON_NAME_INVALID: "Certificate name mismatch loading",
7547
+ ERR_CERT_DATE_INVALID: "Expired certificate loading",
7548
+ ERR_BLOCKED_BY_CLIENT: "Request blocked loading",
7549
+ ERR_FAILED: "Request failed loading",
7550
+ ERR_HTTP_RESPONSE_CODE_FAILURE: "HTTP error loading",
7551
+ NS_ERROR_CONNECTION_REFUSED: "Connection refused loading",
7552
+ NS_ERROR_UNKNOWN_HOST: "DNS lookup failed for",
7553
+ NS_ERROR_NET_TIMEOUT: "Connection timed out loading",
7554
+ NS_ERROR_OFFLINE: "No network while loading",
7555
+ NS_ERROR_ABORT: "Navigation aborted loading",
7556
+ NS_BINDING_ABORTED: "Navigation aborted loading"
7557
+ };
7558
+ function formatTimeout(ms) {
7559
+ if (ms < 1e3) return `${ms} ms`;
7560
+ const seconds = ms / 1e3;
7561
+ const rounded = Math.round(seconds * 10) / 10;
7562
+ return `${Number.isInteger(rounded) ? rounded.toFixed(0) : rounded} s`;
7563
+ }
7564
+ function routeOf(url) {
7565
+ try {
7566
+ const parsed = new URL(url);
7567
+ return `${parsed.pathname}${parsed.search}` || "/";
7568
+ } catch {
7569
+ return url;
7570
+ }
7571
+ }
7572
+ function truncateValue(value, max) {
7573
+ const flat = value.replace(/\s+/g, " ").trim();
7574
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
7575
+ }
7576
+ function countNoun(locator, count) {
7577
+ const role = locator ? /getByRole\(\s*['"]([a-z]+)['"]/.exec(locator)?.[1] : null;
7578
+ const noun = role ?? "element";
7579
+ if (count === 1) return noun;
7580
+ return noun.endsWith("s") ? noun : noun.endsWith("x") || noun.endsWith("ch") ? `${noun}es` : `${noun}s`;
7581
+ }
7582
+ function textOfGetByText(locator) {
7583
+ if (!locator) return null;
7584
+ const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
7585
+ return m ? m[2] : null;
7586
+ }
7587
+ function receivedDisplay(parsed) {
7588
+ const received = parsed.received ?? "";
7589
+ if (parsed.assertion !== "toHaveURL") return received;
7590
+ const m = /^"?(https?:\/\/[^"\s]+)"?$/.exec(received);
7591
+ return m ? `"${routeOf(m[1])}"` : received;
7592
+ }
7593
+ function firstLineFallback(parsed) {
7594
+ const line = (parsed.messageHead.split("\n")[0] ?? "").replace(/^Error:\s*/, "").replace(MASK_TOKEN_RE, "\u2026");
7595
+ return truncateValue(line || "Unknown error", HEADLINE_MAX_CHARS);
7596
+ }
7597
+ var Line = class {
7598
+ constructor() {
7599
+ this.parts = [];
7600
+ }
7601
+ text(text) {
7602
+ if (!text) return this;
7603
+ const last = this.parts[this.parts.length - 1];
7604
+ if (last && last.kind === "text") last.text += text;
7605
+ else this.parts.push({ kind: "text", text });
7606
+ return this;
7607
+ }
7608
+ locator(text) {
7609
+ this.parts.push({ kind: "locator", text });
7610
+ return this;
7611
+ }
7612
+ value(text) {
7613
+ this.parts.push({ kind: "value", text });
7614
+ return this;
7615
+ }
7616
+ toString() {
7617
+ return this.parts.map((p) => p.text).join("");
5526
7618
  }
5527
7619
  };
5528
7620
  function subject(line, locator, opts) {
@@ -5854,7 +7946,7 @@ function testLocation(test) {
5854
7946
  return `${testFile(test)}:${test.location.line}:${test.location.column}`;
5855
7947
  }
5856
7948
  function testFile(test) {
5857
- return path18.relative(process.cwd(), test.location.file).split(path18.sep).join("/");
7949
+ return path23.relative(process.cwd(), test.location.file).split(path23.sep).join("/");
5858
7950
  }
5859
7951
  var PiwiDashboardReporter = class _PiwiDashboardReporter {
5860
7952
  constructor(rawOptions = {}) {
@@ -5882,6 +7974,20 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
5882
7974
  this.filterDetails = null;
5883
7975
  /** Configured `maxFailures` (0 = unlimited) — disambiguates an interrupted run's unrun reason. */
5884
7976
  this.maxFailures = 0;
7977
+ /** The resource censuses the capture fixtures attached, one per test attempt. */
7978
+ this.resourceCensuses = [];
7979
+ /** Lifecycle steps of the tests that ran without the fixtures. */
7980
+ this.lifecycleTallies = [];
7981
+ /** The user fixtures set up by the tests that left a page unused, by test id. */
7982
+ this.fixturesByTest = /* @__PURE__ */ new Map();
7983
+ /** The file workers append their shutdown census to, set in `onBegin`. */
7984
+ this.resourcesFile = null;
7985
+ /** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
7986
+ this.sampler = null;
7987
+ /** Bytes of the files the tests attached, by kind. */
7988
+ this.artifactBytes = emptyArtifacts();
7989
+ /** The run's resource report for the dashboard, built in `onEnd`. */
7990
+ this.resourceReport = null;
5885
7991
  this.streamManager = null;
5886
7992
  /** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
5887
7993
  this.setupSteps = [];
@@ -5941,6 +8047,17 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
5941
8047
  this.startTime = (/* @__PURE__ */ new Date()).toISOString();
5942
8048
  this.playwrightVersion = config.version;
5943
8049
  this.maxFailures = config.maxFailures ?? 0;
8050
+ if (this.options.captureResources !== false) {
8051
+ this.resourcesFile = path23.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
8052
+ process.env[PIWI_RESOURCES_RESULTS_ENV] = this.resourcesFile;
8053
+ try {
8054
+ this.sampler = new RunSampler({ outputDirs: (config.projects ?? []).map((project) => project.outputDir) });
8055
+ this.sampler.start();
8056
+ } catch (error) {
8057
+ this.sampler = null;
8058
+ this.logger.debug(`Run sampler not started: ${errorMessage(error)}`);
8059
+ }
8060
+ }
5944
8061
  this.logger.info(
5945
8062
  `Starting test run for project: ${this.options.projectName} (Playwright v${this.playwrightVersion})`
5946
8063
  );
@@ -6165,6 +8282,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
6165
8282
  }
6166
8283
  }
6167
8284
  }
8285
+ if (this.enabled && this.options.captureResources !== false) this.collectResources(test, result, testCase);
6168
8286
  const reachAttachment = this.options.captureCodeReach === true && this.options.collectPerformanceMetrics !== false ? result.attachments.find((a) => a.name === ATTACHMENT_NAMES.codeReach) : void 0;
6169
8287
  if (reachAttachment?.body) {
6170
8288
  try {
@@ -6209,6 +8327,78 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
6209
8327
  if (this.options.liveFileUploads) this.streamManager.scheduleLiveUpload(testCase);
6210
8328
  }
6211
8329
  }
8330
+ /**
8331
+ * Add up the files a test attached, and keep its resource census; for a test
8332
+ * that ran without the capture fixtures, tally the browsers and contexts its
8333
+ * steps opened and closed.
8334
+ */
8335
+ collectResources(test, result, testCase) {
8336
+ const artifacts = emptyArtifacts();
8337
+ for (const file of result.attachments ?? []) {
8338
+ if (!file.path || INTERNAL_ATTACHMENT_NAMES.has(file.name)) continue;
8339
+ try {
8340
+ const kind = artifactKind(file.name, file.contentType);
8341
+ const bytes = fs21.statSync(file.path).size;
8342
+ artifacts[kind] += bytes;
8343
+ this.artifactBytes[kind] += bytes;
8344
+ } catch {
8345
+ }
8346
+ }
8347
+ const attachment = result.attachments?.find((a) => a.name === ATTACHMENT_NAMES.resources);
8348
+ const census = parseResourceCensus(attachment?.body);
8349
+ if (census) {
8350
+ this.resourceCensuses.push(census);
8351
+ const resources = executionResources(census, artifacts);
8352
+ if (resources) testCase.resources = resources;
8353
+ const leftIdle = census.closed.some((c) => c.used === false) || census.open.some((o) => o.used === false);
8354
+ if (leftIdle) this.fixturesByTest.set(test.id, userFixturesOf(result.steps ?? []));
8355
+ return;
8356
+ }
8357
+ if (!result.steps?.length) return;
8358
+ const tally = tallyLifecycleSteps(testFile(test), result.steps);
8359
+ if (tally.opened.length > 0 || tally.closed.context > 0 || tally.closed.browser > 0)
8360
+ this.lifecycleTallies.push(tally);
8361
+ }
8362
+ /**
8363
+ * Print what the run left open or opened for nothing, from the censuses and
8364
+ * the workers' shutdown file, then what the run cost the machine.
8365
+ */
8366
+ async reportResources() {
8367
+ if (this.options.captureResources === false) return;
8368
+ const profile = this.sampler ? await this.sampler.stop() : null;
8369
+ this.sampler = null;
8370
+ const censuses = [...this.resourceCensuses];
8371
+ if (this.resourcesFile) {
8372
+ try {
8373
+ censuses.push(...parseResourceResults(fs21.readFileSync(this.resourcesFile, "utf8")));
8374
+ fs21.rmSync(this.resourcesFile, { force: true });
8375
+ } catch {
8376
+ }
8377
+ }
8378
+ const lines = [];
8379
+ try {
8380
+ const report = censuses.length > 0 || this.lifecycleTallies.length > 0 ? buildResourceReport({ censuses, fixturesByTest: this.fixturesByTest, tallies: this.lifecycleTallies }) : null;
8381
+ if (report) lines.push(...formatResourceSummary(report, readLeakCheck(this.options.leakCheck)));
8382
+ if (profile) {
8383
+ lines.push(
8384
+ ...formatMachinePanel(profile, { artifacts: this.artifactBytes, workers: workerHealthOf(censuses) })
8385
+ );
8386
+ }
8387
+ if (report || profile) {
8388
+ this.resourceReport = resourceReportWire({
8389
+ report,
8390
+ profile,
8391
+ censuses,
8392
+ artifacts: this.artifactBytes,
8393
+ shardIndex: this.shardInfo?.current ?? null
8394
+ });
8395
+ }
8396
+ } catch (error) {
8397
+ this.logger.debug(`Resource summary skipped: ${errorMessage(error)}`);
8398
+ }
8399
+ for (const line of lines) this.logger.info(line);
8400
+ emitResourceSummary(lines, this.logger);
8401
+ }
6212
8402
  /**
6213
8403
  * Synthesize `didnotrun` cases for tests Playwright planned but never reported
6214
8404
  * (no `onTestEnd`) — typically because `maxFailures` cut the run short. These
@@ -6263,6 +8453,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
6263
8453
  failures: this.failedTests + this.timedOutTests
6264
8454
  });
6265
8455
  this.materializeUnrunTests(unrunReason);
8456
+ await this.reportResources();
6266
8457
  try {
6267
8458
  await this.submitter.submit(
6268
8459
  {
@@ -6282,7 +8473,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
6282
8473
  shardInfo: this.shardInfo,
6283
8474
  setupSteps: this.setupSteps,
6284
8475
  isFullRun: this.isFullRun,
6285
- filterDetails: this.filterDetails
8476
+ filterDetails: this.filterDetails,
8477
+ resourceReport: this.resourceReport
6286
8478
  },
6287
8479
  result
6288
8480
  );
@@ -6366,7 +8558,7 @@ async function resolveSelection(options = {}) {
6366
8558
  }
6367
8559
 
6368
8560
  // src/internal/capture/capture-fixtures.ts
6369
- var path23 = __toESM(require("path"));
8561
+ var path27 = __toESM(require("path"));
6370
8562
  var import_node_zlib2 = require("zlib");
6371
8563
 
6372
8564
  // ../picker-dom/src/probe.ts
@@ -6448,7 +8640,7 @@ function probeElementAttrs(el, arg) {
6448
8640
  };
6449
8641
  const roleMemo = /* @__PURE__ */ new Map();
6450
8642
  const textMemo = /* @__PURE__ */ new Map();
6451
- const roleOf = (n) => {
8643
+ const roleOf2 = (n) => {
6452
8644
  const cached = roleMemo.get(n);
6453
8645
  if (cached !== void 0) return cached;
6454
8646
  let role;
@@ -6471,7 +8663,7 @@ function probeElementAttrs(el, arg) {
6471
8663
  const al = n.getAttribute("aria-level");
6472
8664
  return al && /^\d+$/.test(al) ? Number(al) : null;
6473
8665
  };
6474
- const targetRole = roleOf(el);
8666
+ const targetRole = roleOf2(el);
6475
8667
  const targetLevel = targetRole === "heading" ? levelOf(el) : null;
6476
8668
  const nameOf = (n) => {
6477
8669
  const labelledBy = !!n.getAttribute("aria-labelledby");
@@ -6531,7 +8723,7 @@ function probeElementAttrs(el, arg) {
6531
8723
  let visibleRoleNameCount = 0;
6532
8724
  for (let i = 0; i < nodes.length; i++) {
6533
8725
  const n = nodes[i];
6534
- if (roleOf(n) !== targetRole) continue;
8726
+ if (roleOf2(n) !== targetRole) continue;
6535
8727
  if (n === el) index = roleCountAll;
6536
8728
  roleCountAll++;
6537
8729
  if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
@@ -6584,7 +8776,7 @@ function probeElementAttrs(el, arg) {
6584
8776
  };
6585
8777
  const docRoleCount = (role) => {
6586
8778
  let c = 0;
6587
- for (let i = 0; i < nodes.length; i++) if (roleOf(nodes[i]) === role) c++;
8779
+ for (let i = 0; i < nodes.length; i++) if (roleOf2(nodes[i]) === role) c++;
6588
8780
  return c;
6589
8781
  };
6590
8782
  const rawText = (n) => (n.textContent || "").replace(/\s+/g, " ").trim();
@@ -6616,7 +8808,7 @@ function probeElementAttrs(el, arg) {
6616
8808
  const needle = text.toLowerCase();
6617
8809
  let c = 0;
6618
8810
  for (let i = 0; i < nodes.length; i++) {
6619
- if (roleOf(nodes[i]) !== role) continue;
8811
+ if (roleOf2(nodes[i]) !== role) continue;
6620
8812
  if (normText(nodes[i]).indexOf(needle) !== -1) c++;
6621
8813
  }
6622
8814
  return c;
@@ -6641,7 +8833,7 @@ function probeElementAttrs(el, arg) {
6641
8833
  scopedRoleCount = 0;
6642
8834
  for (let i = 0; i < scoped.length; i++) {
6643
8835
  const n = scoped[i];
6644
- if (roleOf(n) !== targetRole) continue;
8836
+ if (roleOf2(n) !== targetRole) continue;
6645
8837
  if (targetLevel != null && levelOf(n) !== targetLevel) continue;
6646
8838
  scopedRoleCount++;
6647
8839
  }
@@ -6989,7 +9181,7 @@ function showAnchorPicker(arg) {
6989
9181
  }
6990
9182
  const Z = 2147483600;
6991
9183
  const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
6992
- const roleOf = (n) => {
9184
+ const roleOf2 = (n) => {
6993
9185
  const explicit = n.getAttribute && n.getAttribute("role");
6994
9186
  if (explicit) return explicit;
6995
9187
  const tag = (n.tagName || "").toLowerCase();
@@ -7012,7 +9204,7 @@ function showAnchorPicker(arg) {
7012
9204
  let matched = 0;
7013
9205
  for (let i = 0; i < nodes.length; i++) {
7014
9206
  const n = nodes[i];
7015
- if (roleOf(n) !== leafRole) continue;
9207
+ if (roleOf2(n) !== leafRole) continue;
7016
9208
  if (leafLevel != null && levelOf(n) !== leafLevel) continue;
7017
9209
  matched++;
7018
9210
  }
@@ -7045,7 +9237,7 @@ function showAnchorPicker(arg) {
7045
9237
  const testId = node.getAttribute("data-testid");
7046
9238
  const id = node.getAttribute("id");
7047
9239
  const ariaLabel = node.getAttribute("aria-label");
7048
- const role = roleOf(node);
9240
+ const role = roleOf2(node);
7049
9241
  const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
7050
9242
  if (testId) info.testIdCount = count(`[data-testid=${JSON.stringify(testId)}]`);
7051
9243
  if (id) {
@@ -7058,7 +9250,7 @@ function showAnchorPicker(arg) {
7058
9250
  let roleCount = 0;
7059
9251
  let labeledCount = 0;
7060
9252
  for (const n of roleNodes) {
7061
- if (roleOf(n) !== role) continue;
9253
+ if (roleOf2(n) !== role) continue;
7062
9254
  roleCount++;
7063
9255
  if (ariaLabel && n.getAttribute && n.getAttribute("aria-label") === ariaLabel) labeledCount++;
7064
9256
  }
@@ -7107,7 +9299,7 @@ function showAnchorPicker(arg) {
7107
9299
  const nodes = Array.from(scope.querySelectorAll(roleSources));
7108
9300
  if (nodes.length > 2e3) return [];
7109
9301
  return nodes.filter(
7110
- (n) => roleOf(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
9302
+ (n) => roleOf2(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
7111
9303
  );
7112
9304
  } catch {
7113
9305
  return [];
@@ -7760,7 +9952,7 @@ function mergeCandidates(base, extra) {
7760
9952
  }
7761
9953
 
7762
9954
  // src/internal/capture/locator-healing.ts
7763
- var path19 = __toESM(require("path"));
9955
+ var path24 = __toESM(require("path"));
7764
9956
 
7765
9957
  // ../core/src/locator-fingerprint.ts
7766
9958
  var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
@@ -8130,10 +10322,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
8130
10322
  }
8131
10323
  let rel = file;
8132
10324
  try {
8133
- rel = path19.relative(process.cwd(), file);
10325
+ rel = path24.relative(process.cwd(), file);
8134
10326
  } catch {
8135
10327
  }
8136
- rel = rel.split(path19.sep).join("/");
10328
+ rel = rel.split(path24.sep).join("/");
8137
10329
  if (rel.startsWith("./")) rel = rel.slice(2);
8138
10330
  return `${rel}:${m[3]}:${m[4]}`;
8139
10331
  }
@@ -8147,7 +10339,7 @@ function collectPageInventoryInPage(maxEntries) {
8147
10339
  const doc = g.document;
8148
10340
  if (!doc || !g.location) return null;
8149
10341
  const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
8150
- const roleOf = (el) => {
10342
+ const roleOf2 = (el) => {
8151
10343
  const explicit = el.getAttribute("role");
8152
10344
  if (explicit) return explicit.toLowerCase();
8153
10345
  const tag = (el.tagName || "").toLowerCase();
@@ -8201,7 +10393,7 @@ function collectPageInventoryInPage(maxEntries) {
8201
10393
  return text;
8202
10394
  };
8203
10395
  const nameOf = (el) => {
8204
- const role = roleOf(el);
10396
+ const role = roleOf2(el);
8205
10397
  const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
8206
10398
  if (!holdsValue(el, role)) chain.push(el.textContent);
8207
10399
  chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
@@ -8219,7 +10411,7 @@ function collectPageInventoryInPage(maxEntries) {
8219
10411
  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
10412
  for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
8221
10413
  if (budget <= 0) break;
8222
- const role = roleOf(el);
10414
+ const role = roleOf2(el);
8223
10415
  const name = nameOf(el);
8224
10416
  if (!name) continue;
8225
10417
  const key = `${role}\0${name}`;
@@ -8344,7 +10536,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
8344
10536
  }
8345
10537
 
8346
10538
  // src/internal/capture/pick-on-failure.ts
8347
- var path20 = __toESM(require("path"));
10539
+ var path25 = __toESM(require("path"));
8348
10540
  var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
8349
10541
  function parseLeafLocatorExpression(rawExpr) {
8350
10542
  const leaf = parseLeafLocatorCall(rawExpr);
@@ -8361,7 +10553,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE2, "");
8361
10553
  const parsed = parseLeafLocatorExpression(line[1].trim());
8362
10554
  if (!parsed) continue;
8363
10555
  const loc = err.location;
8364
- const location = loc ? `${path20.relative(process.cwd(), loc.file).split(path20.sep).join("/")}:${loc.line}:${loc.column}` : null;
10556
+ const location = loc ? `${path25.relative(process.cwd(), loc.file).split(path25.sep).join("/")}:${loc.line}:${loc.column}` : null;
8365
10557
  return { method: parsed.method, args: parsed.args, location };
8366
10558
  }
8367
10559
  return null;
@@ -8509,64 +10701,11 @@ function applyPickToSnapshots(snapshots, pick) {
8509
10701
  return true;
8510
10702
  }
8511
10703
 
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
10704
  // src/internal/capture/code-reach.ts
8566
10705
  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");
10706
+ var fs22 = __toESM(require("fs"));
10707
+ var path26 = __toESM(require("path"));
10708
+ var import_node_child_process3 = require("child_process");
8570
10709
 
8571
10710
  // ../core/src/source-map.ts
8572
10711
  var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -8821,24 +10960,24 @@ function repositoryRoot(from) {
8821
10960
  if (cachedRepoRoot?.from === from) return cachedRepoRoot.root;
8822
10961
  let root = from;
8823
10962
  try {
8824
- root = (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
10963
+ root = (0, import_node_child_process3.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
8825
10964
  cwd: from,
8826
10965
  encoding: "utf-8",
8827
10966
  stdio: ["ignore", "pipe", "ignore"]
8828
10967
  }).trim();
8829
10968
  } catch {
8830
10969
  }
8831
- cachedRepoRoot = { from, root: path22.resolve(root) };
10970
+ cachedRepoRoot = { from, root: path26.resolve(root) };
8832
10971
  return cachedRepoRoot.root;
8833
10972
  }
8834
10973
  function codeReachRoots(configDir, configured) {
8835
10974
  const repoRoot = repositoryRoot(configDir);
8836
- const roots = configured?.length ? configured.map((r) => path22.resolve(configDir, r)) : [configDir];
10975
+ const roots = configured?.length ? configured.map((r) => path26.resolve(configDir, r)) : [configDir];
8837
10976
  return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
8838
10977
  }
8839
10978
  function isFile(file) {
8840
10979
  try {
8841
- return fs18.statSync(file).isFile();
10980
+ return fs22.statSync(file).isFile();
8842
10981
  } catch {
8843
10982
  return false;
8844
10983
  }
@@ -8846,10 +10985,10 @@ function isFile(file) {
8846
10985
  function resolveSourcePath(source, roots) {
8847
10986
  const normalized = normalizeSourcePath(source);
8848
10987
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
8849
- if (path22.isAbsolute(normalized) && isFile(normalized)) return path22.resolve(normalized);
8850
- const relative11 = normalized.replace(/^\/+/, "");
10988
+ if (path26.isAbsolute(normalized) && isFile(normalized)) return path26.resolve(normalized);
10989
+ const relative13 = normalized.replace(/^\/+/, "");
8851
10990
  for (const root of roots) {
8852
- const candidate = path22.resolve(root, relative11);
10991
+ const candidate = path26.resolve(root, relative13);
8853
10992
  if (isFile(candidate)) return candidate;
8854
10993
  }
8855
10994
  return null;
@@ -8881,7 +11020,7 @@ function fileForUrl(url, roots, bases) {
8881
11020
  return file;
8882
11021
  }
8883
11022
  function relativeToRepo(file, repoRoot) {
8884
- return path22.relative(repoRoot, file).split(path22.sep).join("/");
11023
+ return path26.relative(repoRoot, file).split(path26.sep).join("/");
8885
11024
  }
8886
11025
  function scriptHash(entry) {
8887
11026
  return crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -9071,7 +11210,7 @@ function configuredCodeReachRoots() {
9071
11210
  if (Array.isArray(parsed)) return parsed.filter((r) => typeof r === "string" && r !== "");
9072
11211
  } catch {
9073
11212
  }
9074
- return raw.split(path23.delimiter).filter(Boolean);
11213
+ return raw.split(path27.delimiter).filter(Boolean);
9075
11214
  }
9076
11215
  async function stopSinkCodeReach(sink, closing) {
9077
11216
  const session = sink.codeReach;
@@ -9086,7 +11225,7 @@ async function stopSinkCodeReach(sink, closing) {
9086
11225
  if (!entries) return;
9087
11226
  const info = sink.testInfo;
9088
11227
  const configFile = info?.config.configFile;
9089
- const configDir = configFile ? path23.dirname(configFile) : info?.config.rootDir ?? process.cwd();
11228
+ const configDir = configFile ? path27.dirname(configFile) : info?.config.rootDir ?? process.cwd();
9090
11229
  session.files = await resolveCodeReach(
9091
11230
  entries,
9092
11231
  codeReachRoots(configDir, configuredCodeReachRoots()),
@@ -9102,8 +11241,8 @@ async function drainPendingProbes(capMs) {
9102
11241
  let cap;
9103
11242
  await Promise.race([
9104
11243
  Promise.allSettled(PENDING_PROBES),
9105
- new Promise((resolve6) => {
9106
- cap = setTimeout(resolve6, capMs);
11244
+ new Promise((resolve7) => {
11245
+ cap = setTimeout(resolve7, capMs);
9107
11246
  })
9108
11247
  ]);
9109
11248
  clearTimeout(cap);
@@ -9143,10 +11282,10 @@ async function readWebVitals(page) {
9143
11282
  const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
9144
11283
  paint[key] = Math.round(entry.startTime);
9145
11284
  }
9146
- const readBuffered = (type, extra) => new Promise((resolve6) => {
11285
+ const readBuffered = (type, extra) => new Promise((resolve7) => {
9147
11286
  try {
9148
11287
  const PO = globalThis.PerformanceObserver;
9149
- if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve6(null);
11288
+ if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve7(null);
9150
11289
  const out = [];
9151
11290
  const po = new PO((list) => out.push(...list.getEntries()));
9152
11291
  po.observe({ type, buffered: true, ...extra });
@@ -9156,10 +11295,10 @@ async function readWebVitals(page) {
9156
11295
  po.disconnect();
9157
11296
  } catch {
9158
11297
  }
9159
- resolve6(out);
11298
+ resolve7(out);
9160
11299
  }, 0);
9161
11300
  } catch {
9162
- resolve6(null);
11301
+ resolve7(null);
9163
11302
  }
9164
11303
  });
9165
11304
  const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
@@ -9272,12 +11411,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
9272
11411
  var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
9273
11412
  var MAX_PAGE_INVENTORY_WINDOWS = 64;
9274
11413
  function withTimeout(promise, ms) {
9275
- return new Promise((resolve6, reject) => {
11414
+ return new Promise((resolve7, reject) => {
9276
11415
  const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
9277
11416
  promise.then(
9278
11417
  (value) => {
9279
11418
  clearTimeout(timer);
9280
- resolve6(value);
11419
+ resolve7(value);
9281
11420
  },
9282
11421
  (error) => {
9283
11422
  clearTimeout(timer);
@@ -9650,6 +11789,7 @@ function instrumentPage(page) {
9650
11789
  const original = factories[method].bind(page);
9651
11790
  factories[method] = (...args) => {
9652
11791
  if (currentSink) currentSink.lastActivePage = page;
11792
+ noteResourceUse(page);
9653
11793
  return wrapLocator(page, original(...args), method, args);
9654
11794
  };
9655
11795
  }
@@ -9836,8 +11976,8 @@ async function flushSink(sink, testInfo) {
9836
11976
  let drainDeadline;
9837
11977
  await Promise.race([
9838
11978
  Promise.allSettled(sink.capturePromises),
9839
- new Promise((resolve6) => {
9840
- drainDeadline = setTimeout(resolve6, 2e3);
11979
+ new Promise((resolve7) => {
11980
+ drainDeadline = setTimeout(resolve7, 2e3);
9841
11981
  })
9842
11982
  ]);
9843
11983
  clearTimeout(drainDeadline);
@@ -9979,6 +12119,22 @@ async function flushSink(sink, testInfo) {
9979
12119
  }
9980
12120
  }
9981
12121
  var piwiFixtures = {
12122
+ // Auto, worker-scoped: the worker's resource ledger. Set up before any hook
12123
+ // (worker fixtures come first), so a `beforeAll` context is in it too, and
12124
+ // torn down after the browser closed, so its census sees what only the
12125
+ // worker's shutdown closed. Depends on `playwright` alone, which launches nothing.
12126
+ piwiResources: [
12127
+ async ({ playwright }, use, workerInfo) => {
12128
+ boxCaptureFrames();
12129
+ startResourceLedger(playwright, workerInfo.workerIndex);
12130
+ try {
12131
+ await use();
12132
+ } finally {
12133
+ stopResourceLedger();
12134
+ }
12135
+ },
12136
+ { scope: "worker", auto: true }
12137
+ ],
9982
12138
  // Worker-scoped: patch the shared browser so every page/context created from
9983
12139
  // it — including by user fixtures that take `browser` directly — is captured.
9984
12140
  browser: [
@@ -10026,6 +12182,7 @@ var piwiFixtures = {
10026
12182
  file: testInfo.file,
10027
12183
  titlePath: testInfo.titlePath
10028
12184
  });
12185
+ activeResourceLedger()?.testStarted(testInfo);
10029
12186
  currentSink = sink;
10030
12187
  try {
10031
12188
  await use();
@@ -10052,6 +12209,7 @@ var piwiFixtures = {
10052
12209
  }
10053
12210
  if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
10054
12211
  await flushSink(sink, testInfo);
12212
+ await recordResourceCensus(testInfo);
10055
12213
  }
10056
12214
  },
10057
12215
  { auto: true }
@@ -10083,13 +12241,13 @@ function extendPiwiFixtures(test) {
10083
12241
  }
10084
12242
 
10085
12243
  // src/internal/ai/ai-fixtures.ts
10086
- var fs20 = __toESM(require("fs"));
10087
- var path26 = __toESM(require("path"));
12244
+ var fs24 = __toESM(require("fs"));
12245
+ var path30 = __toESM(require("path"));
10088
12246
  var import_test = require("@playwright/test");
10089
12247
 
10090
12248
  // src/internal/ai/artifact.ts
10091
- var fs19 = __toESM(require("fs"));
10092
- var path24 = __toESM(require("path"));
12249
+ var fs23 = __toESM(require("fs"));
12250
+ var path28 = __toESM(require("path"));
10093
12251
  var ARTIFACT_VERSION = 1;
10094
12252
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
10095
12253
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -10182,7 +12340,7 @@ function parseEntry(text) {
10182
12340
  function readEntry(file) {
10183
12341
  let text;
10184
12342
  try {
10185
- text = fs19.readFileSync(file, "utf8");
12343
+ text = fs23.readFileSync(file, "utf8");
10186
12344
  } catch (error) {
10187
12345
  if (error.code === "ENOENT") return null;
10188
12346
  throw error;
@@ -10192,28 +12350,28 @@ function readEntry(file) {
10192
12350
  function writeEntry(file, entry) {
10193
12351
  const canonical = serializeEntry(entry);
10194
12352
  return withEntryLock(file, () => {
10195
- if (fs19.existsSync(file) && fs19.readFileSync(file, "utf8") === canonical) return { written: false };
10196
- fs19.mkdirSync(path24.dirname(file), { recursive: true });
12353
+ if (fs23.existsSync(file) && fs23.readFileSync(file, "utf8") === canonical) return { written: false };
12354
+ fs23.mkdirSync(path28.dirname(file), { recursive: true });
10197
12355
  const tmp = `${file}.${process.pid}.tmp`;
10198
- fs19.writeFileSync(tmp, canonical);
10199
- fs19.renameSync(tmp, file);
12356
+ fs23.writeFileSync(tmp, canonical);
12357
+ fs23.renameSync(tmp, file);
10200
12358
  return { written: true };
10201
12359
  });
10202
12360
  }
10203
12361
  function withEntryLock(file, fn) {
10204
12362
  const lock = `${file}.lock`;
10205
- fs19.mkdirSync(path24.dirname(file), { recursive: true });
12363
+ fs23.mkdirSync(path28.dirname(file), { recursive: true });
10206
12364
  const deadline = Date.now() + 5e3;
10207
12365
  for (; ; ) {
10208
12366
  try {
10209
- const fd = fs19.openSync(lock, "wx");
10210
- fs19.closeSync(fd);
12367
+ const fd = fs23.openSync(lock, "wx");
12368
+ fs23.closeSync(fd);
10211
12369
  break;
10212
12370
  } catch (error) {
10213
12371
  if (error.code !== "EEXIST") throw error;
10214
12372
  if (Date.now() > deadline) {
10215
12373
  try {
10216
- fs19.unlinkSync(lock);
12374
+ fs23.unlinkSync(lock);
10217
12375
  } catch {
10218
12376
  }
10219
12377
  }
@@ -10223,7 +12381,7 @@ function withEntryLock(file, fn) {
10223
12381
  return fn();
10224
12382
  } finally {
10225
12383
  try {
10226
- fs19.unlinkSync(lock);
12384
+ fs23.unlinkSync(lock);
10227
12385
  } catch {
10228
12386
  }
10229
12387
  }
@@ -10231,7 +12389,7 @@ function withEntryLock(file, fn) {
10231
12389
 
10232
12390
  // src/internal/ai/keys.ts
10233
12391
  var crypto4 = __toESM(require("crypto"));
10234
- var path25 = __toESM(require("path"));
12392
+ var path29 = __toESM(require("path"));
10235
12393
  var DEFAULT_AI_DIR = "__piwi__";
10236
12394
  function normalizeTemplate(template) {
10237
12395
  return template.trim().replace(/\s+/g, " ").toLowerCase();
@@ -10245,13 +12403,13 @@ function slug(text, maxLength = 40) {
10245
12403
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
10246
12404
  }
10247
12405
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
10248
- return path25.join(path25.dirname(specFile), dir, path25.basename(specFile));
12406
+ return path29.join(path29.dirname(specFile), dir, path29.basename(specFile));
10249
12407
  }
10250
12408
  function entryPath(params) {
10251
12409
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
10252
12410
  const hash = hashTemplate(basis, params.ordinal ?? 0);
10253
12411
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
10254
- return path25.join(entryDir(params.specFile, params.dir), name);
12412
+ return path29.join(entryDir(params.specFile, params.dir), name);
10255
12413
  }
10256
12414
  function parseLocation2(location) {
10257
12415
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -10765,7 +12923,7 @@ function testIdentity(testInfo) {
10765
12923
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
10766
12924
  }
10767
12925
  function relativeToCwd(file) {
10768
- return path26.relative(process.cwd(), file).split(path26.sep).join("/");
12926
+ return path30.relative(process.cwd(), file).split(path30.sep).join("/");
10769
12927
  }
10770
12928
  function recordIntents(intents, entry) {
10771
12929
  const add = (locator, kind) => {
@@ -10792,7 +12950,7 @@ function createAiApi(page, testInfo, config, used, intents) {
10792
12950
  const readSource = () => {
10793
12951
  if (source === null) {
10794
12952
  try {
10795
- source = fs20.readFileSync(testInfo.file, "utf8");
12953
+ source = fs24.readFileSync(testInfo.file, "utf8");
10796
12954
  } catch {
10797
12955
  source = "";
10798
12956
  }