@piwitests/reporter 0.44.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,7 @@ var capture_fixtures_exports = {};
32
32
  __export(capture_fixtures_exports, {
33
33
  CAPTURED_ATTRS_ARG: () => CAPTURED_ATTRS_ARG,
34
34
  LocatorPageLog: () => LocatorPageLog,
35
+ activeResourceLedger: () => activeResourceLedger,
35
36
  ariaSnapshotBestEffort: () => ariaSnapshotBestEffort,
36
37
  ariaSnapshotJSONBestEffort: () => ariaSnapshotJSONBestEffort,
37
38
  boxCaptureFrames: () => boxCaptureFrames,
@@ -44,20 +45,26 @@ __export(capture_fixtures_exports, {
44
45
  internalCall: () => internalCall,
45
46
  noteLocatorCall: () => noteLocatorCall,
46
47
  noteNavigation: () => noteNavigation,
48
+ noteResourceUse: () => noteResourceUse,
47
49
  pageMapFetcher: () => pageMapFetcher,
48
50
  piwiFixtures: () => piwiFixtures,
49
51
  probeElementAttrs: () => probeElementAttrs,
52
+ recordResourceCensus: () => recordResourceCensus,
50
53
  resolveCodeReach: () => resolveCodeReach,
51
54
  startCodeReach: () => startCodeReach,
52
- stopCodeReach: () => stopCodeReach
55
+ startResourceLedger: () => startResourceLedger,
56
+ stopCodeReach: () => stopCodeReach,
57
+ stopResourceLedger: () => stopResourceLedger
53
58
  });
54
59
  module.exports = __toCommonJS(capture_fixtures_exports);
55
- var path10 = __toESM(require("path"));
60
+ var path12 = __toESM(require("path"));
56
61
  var import_node_zlib2 = require("zlib");
57
62
 
58
63
  // ../picker-dom/src/probe.ts
59
64
  function probeElementAttrs(el, arg) {
60
65
  const { keep, tagRoles, inputRoles, roleSources, includeStructural } = arg;
66
+ const countMatches = arg.countMatches !== false;
67
+ const testIdAttr = arg.testIdAttribute || "data-testid";
61
68
  const FIELD_TAGS = ["input", "select", "textarea"];
62
69
  const textWithout = (root, skip) => {
63
70
  if (root === skip) return "";
@@ -82,42 +89,45 @@ function probeElementAttrs(el, arg) {
82
89
  }
83
90
  };
84
91
  const attrMap = {};
85
- for (const key of keep) {
92
+ const keys = arg.testIdAttribute && keep.indexOf(arg.testIdAttribute) === -1 ? [...keep, arg.testIdAttribute] : keep;
93
+ for (const key of keys) {
86
94
  const v = el.getAttribute(key) ?? el[key];
87
95
  attrMap[key] = typeof v === "string" ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
88
96
  }
89
97
  const r = el.getBoundingClientRect();
90
98
  const selectorCounts = {};
91
- try {
92
- const doc = el.ownerDocument;
93
- const cssEsc = (s) => doc.defaultView.CSS.escape(s);
94
- const count = (sel) => {
95
- try {
96
- return doc.querySelectorAll(sel).length;
97
- } catch {
98
- return void 0;
99
+ if (countMatches) {
100
+ try {
101
+ const doc = el.ownerDocument;
102
+ const cssEsc = (s) => doc.defaultView.CSS.escape(s);
103
+ const count = (sel) => {
104
+ try {
105
+ return doc.querySelectorAll(sel).length;
106
+ } catch {
107
+ return void 0;
108
+ }
109
+ };
110
+ if (attrMap[testIdAttr]) {
111
+ selectorCounts.testId = count(`[${testIdAttr}=${JSON.stringify(attrMap[testIdAttr])}]`);
99
112
  }
100
- };
101
- if (attrMap["data-testid"]) {
102
- selectorCounts.testId = count(`[data-testid=${JSON.stringify(attrMap["data-testid"])}]`);
103
- }
104
- if (attrMap["id"]) selectorCounts.id = count(`#${cssEsc(attrMap["id"])}`);
105
- if (attrMap["name"]) selectorCounts.name = count(`[name=${JSON.stringify(attrMap["name"])}]`);
106
- if (attrMap["placeholder"]) {
107
- selectorCounts.placeholder = count(`[placeholder=${JSON.stringify(attrMap["placeholder"])}]`);
108
- }
109
- if (attrMap["alt"]) selectorCounts.alt = count(`[alt=${JSON.stringify(attrMap["alt"])}]`);
110
- if (attrMap["title"]) selectorCounts.title = count(`[title=${JSON.stringify(attrMap["title"])}]`);
111
- const classList = (attrMap["class"] || "").split(/\s+/).filter((c) => c.length > 1).slice(0, 10);
112
- if (classList.length > 0) {
113
- const classCounts = {};
114
- for (const cls of classList) {
115
- const n = count(`.${cssEsc(cls)}`);
116
- if (n !== void 0) classCounts[cls] = n;
113
+ if (attrMap["id"]) selectorCounts.id = count(`#${cssEsc(attrMap["id"])}`);
114
+ if (attrMap["name"]) selectorCounts.name = count(`[name=${JSON.stringify(attrMap["name"])}]`);
115
+ if (attrMap["placeholder"]) {
116
+ selectorCounts.placeholder = count(`[placeholder=${JSON.stringify(attrMap["placeholder"])}]`);
117
117
  }
118
- selectorCounts.classes = classCounts;
118
+ if (attrMap["alt"]) selectorCounts.alt = count(`[alt=${JSON.stringify(attrMap["alt"])}]`);
119
+ if (attrMap["title"]) selectorCounts.title = count(`[title=${JSON.stringify(attrMap["title"])}]`);
120
+ const classList = (attrMap["class"] || "").split(/\s+/).filter((c) => c.length > 1).slice(0, 10);
121
+ if (classList.length > 0) {
122
+ const classCounts = {};
123
+ for (const cls of classList) {
124
+ const n = count(`.${cssEsc(cls)}`);
125
+ if (n !== void 0) classCounts[cls] = n;
126
+ }
127
+ selectorCounts.classes = classCounts;
128
+ }
129
+ } catch {
119
130
  }
120
- } catch {
121
131
  }
122
132
  let rolePosition = null;
123
133
  const ancestors = [];
@@ -134,7 +144,7 @@ function probeElementAttrs(el, arg) {
134
144
  };
135
145
  const roleMemo = /* @__PURE__ */ new Map();
136
146
  const textMemo = /* @__PURE__ */ new Map();
137
- const roleOf = (n) => {
147
+ const roleOf2 = (n) => {
138
148
  const cached = roleMemo.get(n);
139
149
  if (cached !== void 0) return cached;
140
150
  let role;
@@ -157,7 +167,7 @@ function probeElementAttrs(el, arg) {
157
167
  const al = n.getAttribute("aria-level");
158
168
  return al && /^\d+$/.test(al) ? Number(al) : null;
159
169
  };
160
- const targetRole = roleOf(el);
170
+ const targetRole = roleOf2(el);
161
171
  const targetLevel = targetRole === "heading" ? levelOf(el) : null;
162
172
  const nameOf = (n) => {
163
173
  const labelledBy = !!n.getAttribute("aria-labelledby");
@@ -206,8 +216,8 @@ function probeElementAttrs(el, arg) {
206
216
  }
207
217
  return total;
208
218
  };
209
- const nodes = doc.querySelectorAll(roleSources);
210
- const nodesUsable = nodes.length <= 4e3;
219
+ const nodes = countMatches ? doc.querySelectorAll(roleSources) : [];
220
+ const nodesUsable = countMatches && nodes.length <= 4e3;
211
221
  const rolesUsable = !!targetRole && nodesUsable;
212
222
  if (rolesUsable) {
213
223
  let roleCountAll = 0;
@@ -217,7 +227,7 @@ function probeElementAttrs(el, arg) {
217
227
  let visibleRoleNameCount = 0;
218
228
  for (let i = 0; i < nodes.length; i++) {
219
229
  const n = nodes[i];
220
- if (roleOf(n) !== targetRole) continue;
230
+ if (roleOf2(n) !== targetRole) continue;
221
231
  if (n === el) index = roleCountAll;
222
232
  roleCountAll++;
223
233
  if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
@@ -241,17 +251,18 @@ function probeElementAttrs(el, arg) {
241
251
  };
242
252
  }
243
253
  }
244
- if (textNeedle) {
254
+ if (textNeedle && countMatches) {
245
255
  const textCount = countTextOwners(doc.body || doc.documentElement, 4e3);
246
256
  if (textCount >= 0) selectorCounts.text = textCount;
247
257
  }
248
- if (rolesUsable || textNeedle) {
258
+ if (rolesUsable || textNeedle || !countMatches) {
249
259
  const CONTAINER_TAGS = ["form", "nav", "main", "article", "section", "dialog", "table", "li", "tr"];
250
260
  const NOISY_DATA_ATTR = /^data-(v-[0-9a-f]+|reactid|react-checksum|svelte-\w+|ng-\w+|ember\w*)$/i;
251
261
  const POSITIONAL_DATA_ATTR = /^data-(index|idx|i|key|row|rownum|col|column|position|pos|order|sort|offset|page)$/i;
252
262
  const TEST_DATA_ATTRS2 = ["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"];
263
+ const testIdName = testIdAttr.toLowerCase();
253
264
  const usableDataAttr = (name, value) => {
254
- if (!name || name.slice(0, 5) !== "data-" || name === "data-testid") return false;
265
+ if (!name || name.slice(0, 5) !== "data-" || name.toLowerCase() === testIdName) return false;
255
266
  if (NOISY_DATA_ATTR.test(name) || POSITIONAL_DATA_ATTR.test(name)) return false;
256
267
  return !!value && value.length <= 120;
257
268
  };
@@ -270,7 +281,7 @@ function probeElementAttrs(el, arg) {
270
281
  };
271
282
  const docRoleCount = (role) => {
272
283
  let c = 0;
273
- for (let i = 0; i < nodes.length; i++) if (roleOf(nodes[i]) === role) c++;
284
+ for (let i = 0; i < nodes.length; i++) if (roleOf2(nodes[i]) === role) c++;
274
285
  return c;
275
286
  };
276
287
  const rawText = (n) => (n.textContent || "").replace(/\s+/g, " ").trim();
@@ -281,7 +292,13 @@ function probeElementAttrs(el, arg) {
281
292
  return digits <= letters;
282
293
  };
283
294
  const usableDiscriminator = (t) => !!t && t.length <= 60 && t !== targetText && namesRatherThanReports(t);
295
+ const hasAtMost = (root, cap) => {
296
+ const walker = doc.createTreeWalker(root, 1);
297
+ for (let n = 0; walker.nextNode(); n++) if (n >= cap) return false;
298
+ return true;
299
+ };
284
300
  const discriminatingText = (anc) => {
301
+ if (!countMatches && !hasAtMost(anc, 200)) return null;
285
302
  const heading = anc.querySelector('h1,h2,h3,h4,h5,h6,[role="heading"]');
286
303
  if (heading) {
287
304
  const t = rawText(heading);
@@ -302,7 +319,7 @@ function probeElementAttrs(el, arg) {
302
319
  const needle = text.toLowerCase();
303
320
  let c = 0;
304
321
  for (let i = 0; i < nodes.length; i++) {
305
- if (roleOf(nodes[i]) !== role) continue;
322
+ if (roleOf2(nodes[i]) !== role) continue;
306
323
  if (normText(nodes[i]).indexOf(needle) !== -1) c++;
307
324
  }
308
325
  return c;
@@ -313,7 +330,7 @@ function probeElementAttrs(el, arg) {
313
330
  depth++;
314
331
  const tag = (node.tagName || "").toLowerCase();
315
332
  if (tag === "body" || tag === "html") break;
316
- const testId = node.getAttribute("data-testid");
333
+ const testId = node.getAttribute(testIdAttr);
317
334
  const id = node.getAttribute("id");
318
335
  const explicitRole = node.getAttribute("role");
319
336
  const ariaLabel = node.getAttribute("aria-label");
@@ -327,16 +344,16 @@ function probeElementAttrs(el, arg) {
327
344
  scopedRoleCount = 0;
328
345
  for (let i = 0; i < scoped.length; i++) {
329
346
  const n = scoped[i];
330
- if (roleOf(n) !== targetRole) continue;
347
+ if (roleOf2(n) !== targetRole) continue;
331
348
  if (targetLevel != null && levelOf(n) !== targetLevel) continue;
332
349
  scopedRoleCount++;
333
350
  }
334
351
  }
335
352
  }
336
- const scopedTextCount = countTextOwners(node, 2e3);
353
+ const scopedTextCount = countMatches ? countTextOwners(node, 2e3) : -1;
337
354
  let filterText = null;
338
355
  try {
339
- if (anchorRole && nodesUsable) filterText = discriminatingText(node);
356
+ if (anchorRole && (nodesUsable || !countMatches)) filterText = discriminatingText(node);
340
357
  } catch {
341
358
  filterText = null;
342
359
  }
@@ -349,11 +366,13 @@ function probeElementAttrs(el, arg) {
349
366
  ariaLabel: ariaLabel || null,
350
367
  ...scopedRoleCount >= 0 ? { scopedRoleCount } : {},
351
368
  ...scopedTextCount >= 0 ? { scopedTextCount } : {},
352
- ...testId ? { testIdCount: count(`[data-testid=${JSON.stringify(testId)}]`) } : {},
353
- ...id ? { idCount: count(`#${cssEsc(id)}`) } : {},
369
+ ...testId && countMatches ? { testIdCount: count(`[${testIdAttr}=${JSON.stringify(testId)}]`) } : {},
370
+ ...id && countMatches ? { idCount: count(`#${cssEsc(id)}`) } : {},
354
371
  ...anchorRole && nodesUsable ? { roleCount: docRoleCount(anchorRole) } : {},
355
- ...filterText ? { filterText, filterRoleCount: filterMatchCount(anchorRole, filterText) } : {},
356
- ...dataAttr ? { dataAttr, dataAttrCount: count(`[${dataAttr.name}=${JSON.stringify(dataAttr.value)}]`) } : {}
372
+ ...filterText ? { filterText } : {},
373
+ ...filterText && countMatches ? { filterRoleCount: filterMatchCount(anchorRole, filterText) } : {},
374
+ ...dataAttr ? { dataAttr } : {},
375
+ ...dataAttr && countMatches ? { dataAttrCount: count(`[${dataAttr.name}=${JSON.stringify(dataAttr.value)}]`) } : {}
357
376
  });
358
377
  }
359
378
  node = node.parentElement;
@@ -391,6 +410,17 @@ function installPickerOverlay(arg) {
391
410
  return;
392
411
  }
393
412
  const Z = 2147483600;
413
+ const testIdAttr = arg.testIdAttribute || "data-testid";
414
+ const words = {
415
+ banner: "Piwi inspector \u2014 click any element to generate locators for it",
416
+ keys: "\u2191 parent \xB7 \u2193 child \xB7 Esc skip",
417
+ keysHovering: "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip",
418
+ analyzing: "Analyzing element\u2026"
419
+ };
420
+ for (const key of Object.keys(words)) {
421
+ const given = arg.strings?.[key];
422
+ if (typeof given === "string" && given) words[key] = given;
423
+ }
394
424
  const highlight = doc.createElement("div");
395
425
  highlight.id = "__piwi_picker_highlight";
396
426
  highlight.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};display:none;box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55),inset 0 0 0 1px rgba(255,255,255,.5);`;
@@ -415,27 +445,53 @@ function installPickerOverlay(arg) {
415
445
  const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
416
446
  const hlLocator = (expr) => `<code style="font-family:${MONO}">${hlTokens(expr)}</code>`;
417
447
  const head = doc.createElement("div");
418
- head.innerHTML = arg.transport === "postMessage" ? "Click an element to generate locators" : arg.failing ? `Piwi locator picker \u2014 click the element that should replace ${hlLocator(arg.failing)}` : "Piwi inspector \u2014 click any element to generate locators for it";
448
+ if (arg.transport === "postMessage") head.textContent = "Click an element to generate locators";
449
+ else if (arg.failing)
450
+ head.innerHTML = `Piwi locator picker \u2014 click the element that should replace ${hlLocator(arg.failing)}`;
451
+ else head.textContent = words.banner;
419
452
  const locatorLine = doc.createElement("div");
420
453
  locatorLine.id = "__piwi_picker_locator";
421
454
  locatorLine.style.cssText = `display:none;margin-top:7px;padding:5px 9px;border-radius:7px;background:#0b1120;border:1px solid #4c1d95;font:13.5px/1.55 ${MONO};word-break:break-word;overflow-wrap:anywhere;`;
422
455
  const foot = doc.createElement("div");
423
456
  foot.id = "__piwi_picker_foot";
424
457
  foot.style.cssText = "color:#9ca3af;margin-top:6px;font-size:12px;";
425
- foot.textContent = "\u2191 parent \xB7 \u2193 child \xB7 Esc skip";
458
+ foot.textContent = words.keys;
426
459
  banner.appendChild(head);
427
460
  banner.appendChild(locatorLine);
428
461
  banner.appendChild(foot);
429
462
  const label = doc.createElement("div");
430
463
  label.id = "__piwi_picker_label";
431
464
  label.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(620px,92vw);background:#0b1120;color:#f9fafb;border:1px solid #7c3aed;border-radius:7px;padding:4px 8px;font:12.5px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
432
- doc.body.appendChild(highlight);
433
- doc.body.appendChild(label);
434
- doc.body.appendChild(banner);
465
+ const mountParent = () => {
466
+ let modal = null;
467
+ try {
468
+ modal = doc.querySelector("dialog:modal");
469
+ } catch {
470
+ modal = null;
471
+ }
472
+ if (!modal) return doc.body;
473
+ const s = g.getComputedStyle(modal);
474
+ const holdsFixed = s.transform !== "none" || s.perspective !== "none" || s.filter !== "none" || /\b(paint|layout|strict|content)\b/.test(s.contain) || /\b(transform|perspective|filter)\b/.test(s.willChange);
475
+ return holdsFixed ? doc.body : modal;
476
+ };
477
+ const mount = (...nodes) => {
478
+ const parent = mountParent();
479
+ for (const node of nodes) parent.appendChild(node);
480
+ if (parent !== doc.body) {
481
+ parent.addEventListener(
482
+ "close",
483
+ () => {
484
+ for (const node of nodes) if (node.parentNode === parent) doc.body.appendChild(node);
485
+ },
486
+ { once: true }
487
+ );
488
+ }
489
+ };
490
+ mount(highlight, label, banner);
435
491
  const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
436
492
  const describe = (el) => {
437
493
  const tag = (el.tagName || "?").toLowerCase();
438
- const testId = el.getAttribute && el.getAttribute("data-testid");
494
+ const testId = el.getAttribute && el.getAttribute(testIdAttr);
439
495
  if (testId) return `getByTestId('${escJs(testId)}')`;
440
496
  if (el.labels && el.labels.length > 0) {
441
497
  const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
@@ -470,7 +526,7 @@ function installPickerOverlay(arg) {
470
526
  const el = chain2[i];
471
527
  const tag = (el.tagName || "").toLowerCase();
472
528
  if (ACTIONABLE_TAGS.includes(tag)) return i;
473
- if (el.getAttribute && (el.getAttribute("role") || el.getAttribute("data-testid"))) return i;
529
+ if (el.getAttribute && (el.getAttribute("role") || el.getAttribute(testIdAttr))) return i;
474
530
  }
475
531
  return 0;
476
532
  };
@@ -526,7 +582,7 @@ function installPickerOverlay(arg) {
526
582
  locatorLine.style.display = "block";
527
583
  }
528
584
  placeLabel(r);
529
- foot.textContent = "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip";
585
+ foot.textContent = words.keysHovering;
530
586
  };
531
587
  const redescribe = () => {
532
588
  labeled = null;
@@ -589,6 +645,13 @@ function installPickerOverlay(arg) {
589
645
  banner.remove();
590
646
  if (g.__piwiRedescribe === redescribe) delete g.__piwiRedescribe;
591
647
  };
648
+ const teardown = () => {
649
+ removePickingListeners();
650
+ removeSuppressed();
651
+ doc.removeEventListener("click", stop, true);
652
+ doc.removeEventListener("keydown", stop, true);
653
+ cleanup();
654
+ };
592
655
  const reportPicked = (el) => {
593
656
  removePickingListeners();
594
657
  highlight.style.display = "none";
@@ -599,13 +662,13 @@ function installPickerOverlay(arg) {
599
662
  g.__piwiSnapshotExtras?.onPick?.();
600
663
  doc.addEventListener("click", stop, true);
601
664
  doc.addEventListener("keydown", stop, true);
602
- foot.textContent = "Analyzing element\u2026";
665
+ foot.textContent = words.analyzing;
603
666
  g.parent.postMessage({ type: "elementPicked", attrs }, "*");
604
667
  } else {
605
668
  removeSuppressed();
606
669
  g.__piwiPickedElement = el;
607
670
  g.__piwiPickState = "picked";
608
- foot.textContent = "Analyzing element\u2026";
671
+ foot.textContent = words.analyzing;
609
672
  }
610
673
  };
611
674
  const reportSkipped = () => {
@@ -653,7 +716,7 @@ function installPickerOverlay(arg) {
653
716
  }
654
717
  });
655
718
  };
656
- g.__piwiPickCleanup = cleanup;
719
+ g.__piwiPickCleanup = teardown;
657
720
  doc.addEventListener("mousemove", onMove, true);
658
721
  doc.addEventListener("click", onClick, true);
659
722
  doc.addEventListener("keydown", onKey, true);
@@ -675,7 +738,27 @@ function showAnchorPicker(arg) {
675
738
  }
676
739
  const Z = 2147483600;
677
740
  const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
678
- const roleOf = (n) => {
741
+ const testIdAttr = arg.testIdAttribute || "data-testid";
742
+ const words = {
743
+ title: "Scope to stable parents (optional)",
744
+ hint: "Pick one or more parents to anchor the locator to. Hover a row to see the parent.",
745
+ noneSelected: "No parents selected \u2014 standard alternatives only.",
746
+ matchesOne: "\u2713 Selection matches exactly 1 element",
747
+ matchesMany: "\u2717 Selection matches {count} elements",
748
+ countUnavailable: "Match count unavailable",
749
+ containsOne: "contains exactly 1 matching element",
750
+ containsMany: "contains {count} matching elements",
751
+ needsTestId: `add a ${testIdAttr} to make this usable`,
752
+ noHook: "no stable hook",
753
+ use: "Use selected parents",
754
+ skip: "Skip (Esc)"
755
+ };
756
+ for (const key of Object.keys(words)) {
757
+ const given = arg.strings?.[key];
758
+ if (typeof given === "string" && given) words[key] = given;
759
+ }
760
+ const counted = (template, n) => template.replace("{count}", String(n));
761
+ const roleOf2 = (n) => {
679
762
  const explicit = n.getAttribute && n.getAttribute("role");
680
763
  if (explicit) return explicit;
681
764
  const tag = (n.tagName || "").toLowerCase();
@@ -690,15 +773,17 @@ function showAnchorPicker(arg) {
690
773
  const al = n.getAttribute && n.getAttribute("aria-level");
691
774
  return al && /^\d+$/.test(al) ? Number(al) : null;
692
775
  };
776
+ const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
693
777
  const leafMatches = (scope) => {
694
778
  try {
695
- if (leafTestId) return scope.querySelectorAll(`[data-testid=${JSON.stringify(leafTestId)}]`).length;
779
+ if (leafTestId) return scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(leafTestId)}]`).length;
780
+ if (!leafCountable) return -1;
696
781
  const nodes = scope.querySelectorAll(roleSources);
697
782
  if (nodes.length > 2e3) return -1;
698
783
  let matched = 0;
699
784
  for (let i = 0; i < nodes.length; i++) {
700
785
  const n = nodes[i];
701
- if (roleOf(n) !== leafRole) continue;
786
+ if (roleOf2(n) !== leafRole) continue;
702
787
  if (leafLevel != null && levelOf(n) !== leafLevel) continue;
703
788
  matched++;
704
789
  }
@@ -728,12 +813,12 @@ function showAnchorPicker(arg) {
728
813
  depth++;
729
814
  const tag = (node.tagName || "").toLowerCase();
730
815
  if (tag === "body" || tag === "html") break;
731
- const testId = node.getAttribute("data-testid");
816
+ const testId = node.getAttribute(testIdAttr);
732
817
  const id = node.getAttribute("id");
733
818
  const ariaLabel = node.getAttribute("aria-label");
734
- const role = roleOf(node);
819
+ const role = roleOf2(node);
735
820
  const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
736
- if (testId) info.testIdCount = count(`[data-testid=${JSON.stringify(testId)}]`);
821
+ if (testId) info.testIdCount = count(`[${testIdAttr}=${JSON.stringify(testId)}]`);
737
822
  if (id) {
738
823
  try {
739
824
  info.idCount = count(`#${doc.defaultView.CSS.escape(id)}`);
@@ -744,7 +829,7 @@ function showAnchorPicker(arg) {
744
829
  let roleCount = 0;
745
830
  let labeledCount = 0;
746
831
  for (const n of roleNodes) {
747
- if (roleOf(n) !== role) continue;
832
+ if (roleOf2(n) !== role) continue;
748
833
  roleCount++;
749
834
  if (ariaLabel && n.getAttribute && n.getAttribute("aria-label") === ariaLabel) labeledCount++;
750
835
  }
@@ -752,7 +837,7 @@ function showAnchorPicker(arg) {
752
837
  if (ariaLabel) info.labeledRoleCount = labeledCount;
753
838
  }
754
839
  info.scopedLeafCount = leafMatches(node);
755
- const hookLabel = testId ? `data-testid="${testId}"` : id ? `#${id}` : ariaLabel && role ? `${role} "${ariaLabel}"` : role ? `role ${role}` : "no stable hook";
840
+ const hookLabel = testId ? `${testIdAttr}="${testId}"` : id ? `#${id}` : ariaLabel && role ? `${role} "${ariaLabel}"` : role ? `role ${role}` : words.noHook;
756
841
  rows.push({
757
842
  node,
758
843
  info,
@@ -774,13 +859,13 @@ function showAnchorPicker(arg) {
774
859
  const pr = el.getBoundingClientRect();
775
860
  pickedOutline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55);left:${pr.left}px;top:${pr.top}px;width:${pr.width}px;height:${pr.height}px;`;
776
861
  const panel = doc.createElement("div");
777
- panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:340px;max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
862
+ panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:min(340px,calc(100vw - 56px));max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
778
863
  const title = doc.createElement("div");
779
864
  title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
780
- title.textContent = "Scope to stable parents (optional)";
865
+ title.textContent = words.title;
781
866
  const sub = doc.createElement("div");
782
867
  sub.style.cssText = "color:#9ca3af;margin-bottom:10px;";
783
- sub.textContent = "Pick one or more parents to anchor the locator to. Hover a row to see the parent.";
868
+ sub.textContent = words.hint;
784
869
  panel.appendChild(title);
785
870
  panel.appendChild(sub);
786
871
  const selected = /* @__PURE__ */ new Set();
@@ -788,12 +873,12 @@ function showAnchorPicker(arg) {
788
873
  footer.style.cssText = "margin:10px 0;font-weight:600;";
789
874
  const segMatches = (scope, info) => {
790
875
  try {
791
- if (info.testId) return Array.from(scope.querySelectorAll(`[data-testid=${JSON.stringify(info.testId)}]`));
876
+ if (info.testId) return Array.from(scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(info.testId)}]`));
792
877
  if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
793
878
  const nodes = Array.from(scope.querySelectorAll(roleSources));
794
879
  if (nodes.length > 2e3) return [];
795
880
  return nodes.filter(
796
- (n) => roleOf(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
881
+ (n) => roleOf2(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
797
882
  );
798
883
  } catch {
799
884
  return [];
@@ -812,14 +897,15 @@ function showAnchorPicker(arg) {
812
897
  let total = 0;
813
898
  for (const s of scopes) {
814
899
  const c = leafMatches(s);
815
- if (c > 0) total += c;
900
+ if (c < 0) return -1;
901
+ total += c;
816
902
  if (total > 50) return total;
817
903
  }
818
904
  return total;
819
905
  };
820
906
  const refreshFooter = () => {
821
907
  if (selected.size === 0) {
822
- footer.textContent = "No parents selected \u2014 standard alternatives only.";
908
+ footer.textContent = words.noneSelected;
823
909
  footer.style.color = "#9ca3af";
824
910
  g.__piwiPickChainCount = void 0;
825
911
  return;
@@ -827,10 +913,10 @@ function showAnchorPicker(arg) {
827
913
  const c = chainCount();
828
914
  g.__piwiPickChainCount = c;
829
915
  if (c === 1) {
830
- footer.textContent = "\u2713 Selection matches exactly 1 element";
916
+ footer.textContent = words.matchesOne;
831
917
  footer.style.color = "#4ade80";
832
918
  } else {
833
- footer.textContent = c < 0 ? "Match count unavailable" : `\u2717 Selection matches ${c} elements`;
919
+ footer.textContent = c < 0 ? words.countUnavailable : counted(words.matchesMany, c);
834
920
  footer.style.color = "#fbbf24";
835
921
  }
836
922
  };
@@ -847,7 +933,8 @@ function showAnchorPicker(arg) {
847
933
  code.textContent = `<${row.info.tag}> ${row.hookLabel}`;
848
934
  const hint = doc.createElement("span");
849
935
  hint.style.cssText = "color:#9ca3af;";
850
- hint.textContent = row.selectable ? row.info.scopedLeafCount === 1 ? "contains exactly 1 matching element" : `contains ${row.info.scopedLeafCount ?? "?"} matching elements` : "add a data-testid to make this usable";
936
+ const inside = row.info.scopedLeafCount ?? -1;
937
+ hint.textContent = row.selectable ? inside === 1 ? words.containsOne : inside < 0 ? words.countUnavailable : counted(words.containsMany, inside) : words.needsTestId;
851
938
  text.appendChild(code);
852
939
  text.appendChild(hint);
853
940
  line.appendChild(box);
@@ -885,6 +972,7 @@ function showAnchorPicker(arg) {
885
972
  outline.remove();
886
973
  outlineLabel.remove();
887
974
  pickedOutline.remove();
975
+ if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
888
976
  };
889
977
  const done = (state) => {
890
978
  g.__piwiPickAnchors = state === "done" ? rows.filter((_, i) => selected.has(i)).map((r) => r.info) : [];
@@ -901,7 +989,7 @@ function showAnchorPicker(arg) {
901
989
  buttonRow.style.cssText = "display:flex;gap:8px;margin-top:4px;";
902
990
  const useBtn = doc.createElement("button");
903
991
  useBtn.style.cssText = "flex:1;background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer;font:600 12px system-ui;";
904
- useBtn.textContent = "Use selected parents";
992
+ useBtn.textContent = words.use;
905
993
  useBtn.addEventListener("click", (e) => {
906
994
  e.preventDefault();
907
995
  e.stopImmediatePropagation();
@@ -909,7 +997,7 @@ function showAnchorPicker(arg) {
909
997
  });
910
998
  const skipBtn = doc.createElement("button");
911
999
  skipBtn.style.cssText = "background:none;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:8px 10px;cursor:pointer;font:12px system-ui;";
912
- skipBtn.textContent = "Skip (Esc)";
1000
+ skipBtn.textContent = words.skip;
913
1001
  skipBtn.addEventListener("click", (e) => {
914
1002
  e.preventDefault();
915
1003
  e.stopImmediatePropagation();
@@ -921,10 +1009,32 @@ function showAnchorPicker(arg) {
921
1009
  refreshFooter();
922
1010
  g.__piwiAnchorCleanup = cleanup;
923
1011
  doc.addEventListener("keydown", onKey, true);
924
- doc.body.appendChild(pickedOutline);
925
- doc.body.appendChild(outline);
926
- doc.body.appendChild(outlineLabel);
927
- doc.body.appendChild(panel);
1012
+ const mountParent = () => {
1013
+ let modal = null;
1014
+ try {
1015
+ modal = doc.querySelector("dialog:modal");
1016
+ } catch {
1017
+ modal = null;
1018
+ }
1019
+ if (!modal) return doc.body;
1020
+ const s = g.getComputedStyle(modal);
1021
+ const holdsFixed = s.transform !== "none" || s.perspective !== "none" || s.filter !== "none" || /\b(paint|layout|strict|content)\b/.test(s.contain) || /\b(transform|perspective|filter)\b/.test(s.willChange);
1022
+ return holdsFixed ? doc.body : modal;
1023
+ };
1024
+ const mount = (...nodes) => {
1025
+ const parent = mountParent();
1026
+ for (const node2 of nodes) parent.appendChild(node2);
1027
+ if (parent !== doc.body) {
1028
+ parent.addEventListener(
1029
+ "close",
1030
+ () => {
1031
+ for (const node2 of nodes) if (node2.parentNode === parent) doc.body.appendChild(node2);
1032
+ },
1033
+ { once: true }
1034
+ );
1035
+ }
1036
+ };
1037
+ mount(pickedOutline, outline, outlineLabel, panel);
928
1038
  }
929
1039
 
930
1040
  // ../picker-dom/src/overlay-confirm.ts
@@ -1140,7 +1250,7 @@ function isAutoGenerated(value) {
1140
1250
  return false;
1141
1251
  }
1142
1252
  var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
1143
- function generateAlternatives(attrs) {
1253
+ function generateAlternatives(attrs, options = {}) {
1144
1254
  const alts = [];
1145
1255
  const seen = /* @__PURE__ */ new Set();
1146
1256
  const add = (loc) => {
@@ -1151,12 +1261,14 @@ function generateAlternatives(attrs) {
1151
1261
  };
1152
1262
  const { accessibleName } = attrs;
1153
1263
  const tag = attrs.tagName;
1154
- const role = resolveAriaRole(attrs);
1264
+ const role = options.role !== void 0 ? options.role : resolveAriaRole(attrs);
1155
1265
  const counts = attrs.selectorCounts;
1156
1266
  const isUnique = (n) => n == null || n <= 1;
1157
1267
  const ambiguityPenalty = (n) => n != null && n > 1 ? 45 : 0;
1158
1268
  const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
1159
- const testId = attr(attrs, "data-testid");
1269
+ const testIdAttribute = options.testIdAttribute || "data-testid";
1270
+ const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
1271
+ const testId = attr(attrs, testIdAttribute);
1160
1272
  if (testId && isUnique(counts?.testId)) {
1161
1273
  add({
1162
1274
  locator: `getByTestId('${esc(testId)}')`,
@@ -1215,7 +1327,7 @@ function generateAlternatives(attrs) {
1215
1327
  });
1216
1328
  }
1217
1329
  const id = attr(attrs, "id");
1218
- if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
1330
+ if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
1219
1331
  const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
1220
1332
  add({
1221
1333
  locator: `locator('${esc(selector)}')`,
@@ -1225,7 +1337,7 @@ function generateAlternatives(attrs) {
1225
1337
  });
1226
1338
  }
1227
1339
  const name = attr(attrs, "name");
1228
- if (name && isUnique(counts?.name)) {
1340
+ if (name && selectable("name") && isUnique(counts?.name)) {
1229
1341
  const selector = `[name="${escCssAttrValue(name)}"]`;
1230
1342
  add({
1231
1343
  locator: `locator('${esc(selector)}')`,
@@ -1270,7 +1382,7 @@ function generateAlternatives(attrs) {
1270
1382
  score: scores.testId
1271
1383
  });
1272
1384
  }
1273
- if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
1385
+ if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
1274
1386
  idAnchorDone = true;
1275
1387
  const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
1276
1388
  add({
@@ -1280,7 +1392,7 @@ function generateAlternatives(attrs) {
1280
1392
  score: scores.id
1281
1393
  });
1282
1394
  }
1283
- if (!dataAnchorDone && anc.dataAttr && anc.dataAttrCount === 1) {
1395
+ if (!dataAnchorDone && anc.dataAttr && selectable(anc.dataAttr.name) && anc.dataAttrCount === 1) {
1284
1396
  dataAnchorDone = true;
1285
1397
  const { name: name2, value } = anc.dataAttr;
1286
1398
  const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
@@ -1798,7 +1910,8 @@ var ATTACHMENT_NAMES = {
1798
1910
  pageState: "piwi-page-state",
1799
1911
  userPick: "piwi-user-pick",
1800
1912
  aiUsage: "piwi-ai-usage",
1801
- aiMeta: "piwi-ai-meta"
1913
+ aiMeta: "piwi-ai-meta",
1914
+ resources: "piwi-resources"
1802
1915
  };
1803
1916
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
1804
1917
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
@@ -1811,7 +1924,7 @@ function collectPageInventoryInPage(maxEntries) {
1811
1924
  const doc = g.document;
1812
1925
  if (!doc || !g.location) return null;
1813
1926
  const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
1814
- const roleOf = (el) => {
1927
+ const roleOf2 = (el) => {
1815
1928
  const explicit = el.getAttribute("role");
1816
1929
  if (explicit) return explicit.toLowerCase();
1817
1930
  const tag = (el.tagName || "").toLowerCase();
@@ -1865,7 +1978,7 @@ function collectPageInventoryInPage(maxEntries) {
1865
1978
  return text;
1866
1979
  };
1867
1980
  const nameOf = (el) => {
1868
- const role = roleOf(el);
1981
+ const role = roleOf2(el);
1869
1982
  const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
1870
1983
  if (!holdsValue(el, role)) chain.push(el.textContent);
1871
1984
  chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
@@ -1883,7 +1996,7 @@ function collectPageInventoryInPage(maxEntries) {
1883
1996
  const controlSelector = 'button, [role="button"], input, select, textarea, [role="checkbox"], [role="radio"], [role="tab"], [role="menuitem"], [role="combobox"], [role="switch"], [role="slider"], [role="searchbox"], [role="spinbutton"], [role="textbox"], [role="listbox"]';
1884
1997
  for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
1885
1998
  if (budget <= 0) break;
1886
- const role = roleOf(el);
1999
+ const role = roleOf2(el);
1887
2000
  const name = nameOf(el);
1888
2001
  if (!name) continue;
1889
2002
  const key = `${role}\0${name}`;
@@ -2038,9 +2151,12 @@ var DEFAULTS = {
2038
2151
  // Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
2039
2152
  captureCodeReach: false,
2040
2153
  captureServerTraces: true,
2154
+ captureResources: true,
2155
+ leakCheck: "report",
2041
2156
  sampleAriaOnPass: true,
2042
2157
  uploadManifest: true,
2043
2158
  defaultCapture: true,
2159
+ checkBaseUrl: false,
2044
2160
  streaming: true,
2045
2161
  streamingBatchSize: 5,
2046
2162
  streamingBatchDelay: 2e3,
@@ -2078,9 +2194,12 @@ var PIWI_ENV_KEYS = {
2078
2194
  captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
2079
2195
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
2080
2196
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
2197
+ captureResources: "PIWI_CAPTURE_RESOURCES",
2198
+ leakCheck: "PIWI_LEAK_CHECK",
2081
2199
  sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
2082
2200
  uploadManifest: "PIWI_UPLOAD_MANIFEST",
2083
2201
  defaultCapture: "PIWI_DEFAULT_CAPTURE",
2202
+ checkBaseUrl: "PIWI_CHECK_BASE_URL",
2084
2203
  inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
2085
2204
  pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
2086
2205
  outputFile: "PIWI_OUTPUT_FILE",
@@ -2103,6 +2222,7 @@ var PIWI_FLAKE_ENV = {
2103
2222
  plan: "PIWI_FLAKE_PLAN",
2104
2223
  results: "PIWI_FLAKE_RESULTS"
2105
2224
  };
2225
+ var PIWI_RESOURCES_RESULTS_ENV = "PIWI_RESOURCES_RESULTS";
2106
2226
  var ENV_FALLBACK_SPECS = [
2107
2227
  { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
2108
2228
  { option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
@@ -2127,9 +2247,12 @@ var ENV_FALLBACK_SPECS = [
2127
2247
  { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
2128
2248
  { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
2129
2249
  { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
2250
+ { option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
2251
+ { option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
2130
2252
  { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
2131
2253
  { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
2132
2254
  { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
2255
+ { option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
2133
2256
  { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
2134
2257
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
2135
2258
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
@@ -2142,8 +2265,8 @@ function parseRouteKey(routeKey2) {
2142
2265
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
2143
2266
  }
2144
2267
  function patternToRegex(pattern) {
2145
- const path11 = pattern.split("?")[0] ?? pattern;
2146
- const escaped = path11.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2268
+ const path13 = pattern.split("?")[0] ?? pattern;
2269
+ const escaped = path13.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2147
2270
  return new RegExp(`^${escaped}$`);
2148
2271
  }
2149
2272
  function suitePathEqual(a, b) {
@@ -2166,13 +2289,13 @@ function matchProbeItem(plan, test) {
2166
2289
  function requestMatchesRoute(method, url, routeKey2) {
2167
2290
  const { method: rm, pattern } = parseRouteKey(routeKey2);
2168
2291
  if (rm && rm !== method.toUpperCase()) return false;
2169
- let path11;
2292
+ let path13;
2170
2293
  try {
2171
- path11 = new URL(url).pathname;
2294
+ path13 = new URL(url).pathname;
2172
2295
  } catch {
2173
- path11 = url.split("?")[0] ?? url;
2296
+ path13 = url.split("?")[0] ?? url;
2174
2297
  }
2175
- return patternToRegex(pattern).test(path11);
2298
+ return patternToRegex(pattern).test(path13);
2176
2299
  }
2177
2300
  function createProbeState() {
2178
2301
  return { navigated: false, matches: 0 };
@@ -2194,10 +2317,10 @@ function isProbeMode() {
2194
2317
  function loadPlan() {
2195
2318
  if (planCache !== void 0) return planCache;
2196
2319
  planCache = null;
2197
- const path11 = process.env[PIWI_PROBE_ENV.plan];
2198
- if (path11) {
2320
+ const path13 = process.env[PIWI_PROBE_ENV.plan];
2321
+ if (path13) {
2199
2322
  try {
2200
- planCache = JSON.parse(fs2.readFileSync(path11, "utf8"));
2323
+ planCache = JSON.parse(fs2.readFileSync(path13, "utf8"));
2201
2324
  } catch {
2202
2325
  planCache = null;
2203
2326
  }
@@ -2219,10 +2342,10 @@ function outcomeFromStatus(status, applied) {
2219
2342
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2220
2343
  }
2221
2344
  function recordProbeOutcome(line) {
2222
- const path11 = process.env[PIWI_PROBE_ENV.results];
2223
- if (!path11) return;
2345
+ const path13 = process.env[PIWI_PROBE_ENV.results];
2346
+ if (!path13) return;
2224
2347
  try {
2225
- fs2.appendFileSync(path11, `${JSON.stringify(line)}
2348
+ fs2.appendFileSync(path13, `${JSON.stringify(line)}
2226
2349
  `);
2227
2350
  } catch {
2228
2351
  }
@@ -3496,12 +3619,23 @@ var BOXED_PREFIX_EXPORTS = [
3496
3619
  ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
3497
3620
  ];
3498
3621
  var boxing;
3622
+ var resolvedPlaywrightPackageJson;
3623
+ function resolvePlaywrightPackageJson() {
3624
+ if (resolvedPlaywrightPackageJson !== void 0) return resolvedPlaywrightPackageJson;
3625
+ try {
3626
+ const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
3627
+ resolvedPlaywrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
3628
+ } catch {
3629
+ resolvedPlaywrightPackageJson = null;
3630
+ }
3631
+ return resolvedPlaywrightPackageJson;
3632
+ }
3499
3633
  function resolveBoxing() {
3500
3634
  try {
3501
3635
  const root = findOwnPackageJson(__dirname)?.root;
3502
3636
  if (!root) return null;
3503
- const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
3504
- const playwrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
3637
+ const playwrightPackageJson = resolvePlaywrightPackageJson();
3638
+ if (!playwrightPackageJson) return null;
3505
3639
  const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
3506
3640
  for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
3507
3641
  let set;
@@ -3823,9 +3957,9 @@ function resolveSourcePath(source, roots) {
3823
3957
  const normalized = normalizeSourcePath(source);
3824
3958
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
3825
3959
  if (path9.isAbsolute(normalized) && isFile(normalized)) return path9.resolve(normalized);
3826
- const relative7 = normalized.replace(/^\/+/, "");
3960
+ const relative8 = normalized.replace(/^\/+/, "");
3827
3961
  for (const root of roots) {
3828
- const candidate = path9.resolve(root, relative7);
3962
+ const candidate = path9.resolve(root, relative8);
3829
3963
  if (isFile(candidate)) return candidate;
3830
3964
  }
3831
3965
  return null;
@@ -3975,6 +4109,871 @@ async function stopCodeReach(page) {
3975
4109
  }
3976
4110
  }
3977
4111
 
4112
+ // src/internal/capture/resource-ledger.ts
4113
+ var fs8 = __toESM(require("fs"));
4114
+ var path11 = __toESM(require("path"));
4115
+ var import_node_module2 = require("module");
4116
+
4117
+ // src/internal/capture/worker-metrics.ts
4118
+ var import_node_perf_hooks = require("perf_hooks");
4119
+
4120
+ // src/internal/support/system-readers.ts
4121
+ var fs7 = __toESM(require("fs"));
4122
+ var path10 = __toESM(require("path"));
4123
+ var CLK_TCK = 100;
4124
+ function readText(file) {
4125
+ try {
4126
+ return fs7.readFileSync(file, "utf8");
4127
+ } catch {
4128
+ return null;
4129
+ }
4130
+ }
4131
+ function parseProcStat(pid, text) {
4132
+ const open = text.indexOf("(");
4133
+ const close = text.lastIndexOf(")");
4134
+ if (open < 0 || close < open) return null;
4135
+ const fields = text.slice(close + 2).trim().split(/\s+/);
4136
+ const ppid = Number(fields[1]);
4137
+ const utime = Number(fields[11]);
4138
+ const stime = Number(fields[12]);
4139
+ const start = Number(fields[19]);
4140
+ if (![ppid, utime, stime, start].every(Number.isFinite)) return null;
4141
+ return { pid, ppid, comm: text.slice(open + 1, close), cpuMs: (utime + stime) * 1e3 / CLK_TCK, start };
4142
+ }
4143
+ function parseSchedstat(text) {
4144
+ const waitNs = Number(text.trim().split(/\s+/)[1]);
4145
+ return Number.isFinite(waitNs) ? waitNs / 1e6 : null;
4146
+ }
4147
+ function parseKbField(text, key) {
4148
+ const match = new RegExp(`^${key}:\\s+(\\d+)`, "m").exec(text);
4149
+ return match ? Number(match[1]) : null;
4150
+ }
4151
+ function parseCpuTicks(text) {
4152
+ const line = text.split("\n", 1)[0] ?? "";
4153
+ if (!line.startsWith("cpu ")) return null;
4154
+ const v = line.trim().split(/\s+/).slice(1, 9).map(Number);
4155
+ if (v.length < 8 || v.some((n) => !Number.isFinite(n))) return null;
4156
+ const [user, nice, system, idle, iowait, irq, softirq, steal] = v;
4157
+ return { busy: user + nice + system + irq + softirq, idle, iowait, steal, total: v.reduce((sum, n) => sum + n, 0) };
4158
+ }
4159
+ function parsePsiTotal(text) {
4160
+ const match = /^some\b.*\btotal=(\d+)/m.exec(text);
4161
+ return match ? Number(match[1]) : null;
4162
+ }
4163
+ function roleOf(comm, cmdline) {
4164
+ const command = `${comm} ${cmdline}`;
4165
+ if (/workerProcessEntry/.test(cmdline)) return "worker";
4166
+ if (/loaderProcessEntry/.test(cmdline)) return "runner";
4167
+ if (/(^|[\s/])ffmpeg/.test(command)) return "ffmpeg";
4168
+ const type = /--type=([\w-]+)/.exec(cmdline)?.[1];
4169
+ if (/chrom|headless_shell|msedge|Google Chrome/i.test(command)) {
4170
+ if (!type) return "browser";
4171
+ if (type === "renderer") return "renderer";
4172
+ if (type === "gpu-process") return "gpu";
4173
+ return "utility";
4174
+ }
4175
+ if (/firefox/i.test(command)) return /-contentproc/.test(cmdline) ? "renderer" : "browser";
4176
+ if (/WebKitWebProcess/.test(command)) return "renderer";
4177
+ if (/WebKitGPUProcess/.test(command)) return "gpu";
4178
+ if (/WebKitNetworkProcess/.test(command)) return "utility";
4179
+ if (/MiniBrowser|pw_run\.sh/.test(command)) return "browser";
4180
+ return null;
4181
+ }
4182
+ function descendantsOf(root, table) {
4183
+ const children = /* @__PURE__ */ new Map();
4184
+ for (const proc of table) {
4185
+ const list = children.get(proc.ppid) ?? [];
4186
+ list.push(proc);
4187
+ children.set(proc.ppid, list);
4188
+ }
4189
+ const out = [];
4190
+ const stack = [root];
4191
+ const seen = /* @__PURE__ */ new Set([root]);
4192
+ while (stack.length > 0) {
4193
+ for (const child of children.get(stack.pop()) ?? []) {
4194
+ if (seen.has(child.pid)) continue;
4195
+ seen.add(child.pid);
4196
+ out.push(child);
4197
+ stack.push(child.pid);
4198
+ }
4199
+ }
4200
+ return out;
4201
+ }
4202
+ var ProcFs = class {
4203
+ constructor(root = "/proc") {
4204
+ this.root = root;
4205
+ }
4206
+ /** Whether this looks like a Linux `/proc` at all. */
4207
+ available() {
4208
+ return fs7.existsSync(path10.join(this.root, "stat"));
4209
+ }
4210
+ /** Every process, keyed by pid. */
4211
+ table() {
4212
+ const out = /* @__PURE__ */ new Map();
4213
+ let names;
4214
+ try {
4215
+ names = fs7.readdirSync(this.root);
4216
+ } catch {
4217
+ return out;
4218
+ }
4219
+ for (const name of names) {
4220
+ if (!/^\d+$/.test(name)) continue;
4221
+ const text = readText(path10.join(this.root, name, "stat"));
4222
+ const stat = text ? parseProcStat(Number(name), text) : null;
4223
+ if (stat) out.set(stat.pid, stat);
4224
+ }
4225
+ return out;
4226
+ }
4227
+ stat(pid) {
4228
+ const text = readText(path10.join(this.root, String(pid), "stat"));
4229
+ return text ? parseProcStat(pid, text) : null;
4230
+ }
4231
+ runWaitMs(pid) {
4232
+ const text = readText(path10.join(this.root, String(pid), "schedstat"));
4233
+ return text ? parseSchedstat(text) : null;
4234
+ }
4235
+ cmdline(pid) {
4236
+ return (readText(path10.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
4237
+ }
4238
+ /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
4239
+ statusKb(pid, key) {
4240
+ const text = readText(path10.join(this.root, String(pid), "status"));
4241
+ return text ? parseKbField(text, key) : null;
4242
+ }
4243
+ /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
4244
+ async pssKb(pid) {
4245
+ try {
4246
+ return parseKbField(await fs7.promises.readFile(path10.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
4247
+ } catch {
4248
+ return null;
4249
+ }
4250
+ }
4251
+ /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
4252
+ resetPeakRss(pid) {
4253
+ try {
4254
+ fs7.writeFileSync(path10.join(this.root, String(pid), "clear_refs"), "5");
4255
+ return true;
4256
+ } catch {
4257
+ return false;
4258
+ }
4259
+ }
4260
+ cpuTicks() {
4261
+ const text = readText(path10.join(this.root, "stat"));
4262
+ return text ? parseCpuTicks(text) : null;
4263
+ }
4264
+ /** Total stall time of a resource's pressure file, in µs. */
4265
+ pressureUs(resource) {
4266
+ const text = readText(path10.join(this.root, "pressure", resource));
4267
+ return text ? parsePsiTotal(text) : null;
4268
+ }
4269
+ memAvailableKb() {
4270
+ const text = readText(path10.join(this.root, "meminfo"));
4271
+ return text ? parseKbField(text, "MemAvailable") : null;
4272
+ }
4273
+ /** Open file descriptors of a process. */
4274
+ fdCount(pid) {
4275
+ try {
4276
+ return fs7.readdirSync(path10.join(this.root, String(pid), "fd")).length;
4277
+ } catch {
4278
+ return null;
4279
+ }
4280
+ }
4281
+ };
4282
+ var UNLIMITED = 2 ** 60;
4283
+
4284
+ // src/internal/capture/worker-metrics.ts
4285
+ var MB = 1024 * 1024;
4286
+ function round1(value) {
4287
+ return Math.round(value * 10) / 10;
4288
+ }
4289
+ var WorkerMetrics = class {
4290
+ constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
4291
+ this.proc = proc;
4292
+ this.pid = pid;
4293
+ this.loop = null;
4294
+ this.usage = null;
4295
+ /** Each process's CPU and wait at the start of the current window; null before the first. */
4296
+ this.subtree = null;
4297
+ this.roles = /* @__PURE__ */ new Map();
4298
+ this.linux = platform === "linux" && proc.available();
4299
+ let histogram = null;
4300
+ try {
4301
+ histogram = (0, import_node_perf_hooks.monitorEventLoopDelay)({ resolution: 20 });
4302
+ histogram.enable();
4303
+ } catch {
4304
+ histogram = null;
4305
+ }
4306
+ this.histogram = histogram;
4307
+ }
4308
+ /** The worker's processes under it, keyed by pid and start time. */
4309
+ tree() {
4310
+ const table = this.proc.table();
4311
+ return descendantsOf(this.pid, table.values()).map((stat) => {
4312
+ const key = `${stat.pid}:${stat.start}`;
4313
+ let role = this.roles.get(key);
4314
+ if (!role) {
4315
+ role = roleOf(stat.comm, this.proc.cmdline(stat.pid)) ?? "other";
4316
+ this.roles.set(key, role);
4317
+ }
4318
+ return { key, pid: stat.pid, cpuMs: stat.cpuMs, role };
4319
+ });
4320
+ }
4321
+ /**
4322
+ * The processes under the worker now, and the reset of their peak memory: the
4323
+ * start of the next window. Taken once per test, at its end, so the time
4324
+ * between two tests counts toward the next one.
4325
+ */
4326
+ snapshot() {
4327
+ this.subtree = /* @__PURE__ */ new Map();
4328
+ for (const proc of this.tree()) {
4329
+ this.subtree.set(proc.key, { cpuMs: proc.cpuMs, waitMs: this.proc.runWaitMs(proc.pid) });
4330
+ this.proc.resetPeakRss(proc.pid);
4331
+ }
4332
+ }
4333
+ /**
4334
+ * Start a test's window. `processes` says whether the worker has started any
4335
+ * browser to read; a worker that never did skips the process tree.
4336
+ */
4337
+ start(processes = true) {
4338
+ try {
4339
+ this.histogram?.reset();
4340
+ this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
4341
+ this.usage = process.resourceUsage();
4342
+ if (this.linux && processes && !this.subtree) this.snapshot();
4343
+ } catch {
4344
+ }
4345
+ }
4346
+ /** Close the window. Null when the test's start was never read. */
4347
+ end(processes = true) {
4348
+ if (!this.usage || !this.loop) return null;
4349
+ try {
4350
+ const usage = process.resourceUsage();
4351
+ const loop = import_node_perf_hooks.performance.eventLoopUtilization(this.loop);
4352
+ const metrics = {
4353
+ worker: {
4354
+ cpuMs: Math.round(
4355
+ (usage.userCPUTime - this.usage.userCPUTime + usage.systemCPUTime - this.usage.systemCPUTime) / 1e3
4356
+ ),
4357
+ involuntarySwitches: usage.involuntaryContextSwitches - this.usage.involuntaryContextSwitches,
4358
+ loopUtilization: Math.round(loop.utilization * 1e3) / 1e3,
4359
+ loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
4360
+ loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
4361
+ heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
4362
+ fds: this.linux ? this.proc.fdCount(this.pid) : null
4363
+ }
4364
+ };
4365
+ this.usage = null;
4366
+ this.loop = null;
4367
+ if (this.linux && processes) metrics.roles = this.roleCosts();
4368
+ return metrics;
4369
+ } catch {
4370
+ return null;
4371
+ }
4372
+ }
4373
+ /** The window's cost by role; what it reads becomes the start of the next window. */
4374
+ roleCosts() {
4375
+ const roles = {};
4376
+ const start = this.subtree ?? /* @__PURE__ */ new Map();
4377
+ const next = /* @__PURE__ */ new Map();
4378
+ for (const proc of this.tree()) {
4379
+ const from = start.get(proc.key) ?? { cpuMs: 0, waitMs: 0 };
4380
+ const waitMs = this.proc.runWaitMs(proc.pid);
4381
+ const peakKb = this.proc.statusKb(proc.pid, "VmHWM");
4382
+ const cost = roles[proc.role] ??= { cpuMs: 0, runWaitMs: null, peakRssMb: null, processes: 0 };
4383
+ cost.cpuMs += Math.max(0, proc.cpuMs - from.cpuMs);
4384
+ if (waitMs !== null && from.waitMs !== null)
4385
+ cost.runWaitMs = round1((cost.runWaitMs ?? 0) + Math.max(0, waitMs - from.waitMs));
4386
+ if (peakKb !== null) cost.peakRssMb = Math.max(cost.peakRssMb ?? 0, round1(peakKb / 1024));
4387
+ cost.processes++;
4388
+ next.set(proc.key, { cpuMs: proc.cpuMs, waitMs });
4389
+ this.proc.resetPeakRss(proc.pid);
4390
+ }
4391
+ this.subtree = next;
4392
+ return roles;
4393
+ }
4394
+ dispose() {
4395
+ this.histogram?.disable();
4396
+ }
4397
+ };
4398
+ var PageReader = class {
4399
+ constructor() {
4400
+ this.sessions = /* @__PURE__ */ new WeakMap();
4401
+ }
4402
+ async read(page) {
4403
+ let session = this.sessions.get(page);
4404
+ if (!session) {
4405
+ session = internalCall(page, async () => {
4406
+ const opened2 = await page.context().newCDPSession(page);
4407
+ await opened2.send("Performance.enable", { timeDomain: "threadTicks" });
4408
+ return opened2;
4409
+ }).catch(() => null);
4410
+ this.sessions.set(page, session);
4411
+ }
4412
+ const opened = await session;
4413
+ if (!opened) return null;
4414
+ try {
4415
+ const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
4416
+ const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
4417
+ return {
4418
+ cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
4419
+ heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
4420
+ nodes: values.get("Nodes") ?? 0,
4421
+ listeners: values.get("JSEventListeners") ?? 0
4422
+ };
4423
+ } catch {
4424
+ return null;
4425
+ }
4426
+ }
4427
+ };
4428
+
4429
+ // src/internal/capture/resource-ledger.ts
4430
+ var TRACKED_HANDLE_TYPES = ["TCPServerWrap", "FSEventWrap", "StatWatcher"];
4431
+ var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
4432
+ var CLOSE_TIMEOUT_MS = 5e3;
4433
+ var PAGE_READ_CAP = 50;
4434
+ var PAGE_READ_TIMEOUT_MS = 1e3;
4435
+ var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
4436
+ var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
4437
+ function relativePath(file) {
4438
+ return path11.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path11.sep).join("/");
4439
+ }
4440
+ function isLibraryFile(file) {
4441
+ return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path11.sep}node_modules${path11.sep}`) || file.includes("/node_modules/") || file === __filename;
4442
+ }
4443
+ function userSite(stack) {
4444
+ for (const line of (stack ?? "").split("\n").slice(1)) {
4445
+ const match = /^\s*at\s+(?:.*?\s+\()?(.+?):(\d+):\d+\)?$/.exec(line);
4446
+ if (!match) continue;
4447
+ const file = match[1].replace(/^file:\/\//, "");
4448
+ if (isLibraryFile(file) || !/\.[a-z]+$/i.test(file)) continue;
4449
+ return `${relativePath(file)}:${match[2]}`;
4450
+ }
4451
+ return null;
4452
+ }
4453
+ function zoneSite(zone) {
4454
+ for (const frame of zone?.frames ?? []) {
4455
+ if (!frame?.file || isLibraryFile(frame.file)) continue;
4456
+ return `${relativePath(frame.file)}:${frame.line ?? 0}`;
4457
+ }
4458
+ return null;
4459
+ }
4460
+ function testRef2(info) {
4461
+ if (!info?.testId) return null;
4462
+ const titlePath = Array.isArray(info.titlePath) ? info.titlePath : [];
4463
+ return {
4464
+ id: info.testId,
4465
+ file: info.file ? relativePath(info.file) : "",
4466
+ suite: titlePath.slice(1, -1).map(String)
4467
+ };
4468
+ }
4469
+ function runnableOf(info) {
4470
+ if (!info) return { phase: "worker", fixture: null };
4471
+ const runnable = info._timeoutManager?._running?.runnable;
4472
+ const type = typeof runnable?.type === "string" ? runnable.type : "test";
4473
+ const phase = HOOK_PHASES.has(type) ? type : type === "teardown" ? "worker" : "test";
4474
+ const raw = runnable?.fixture;
4475
+ const fixture = raw && typeof raw.title === "string" ? {
4476
+ title: raw.title,
4477
+ location: raw.location?.file ? `${relativePath(raw.location.file)}:${raw.location.line ?? 0}` : null,
4478
+ worker: raw.slot !== void 0 && raw.slot !== null
4479
+ } : null;
4480
+ return { phase, fixture };
4481
+ }
4482
+ function fallbackRunnable(window) {
4483
+ return { phase: window ? "test" : "worker", fixture: null };
4484
+ }
4485
+ function playwrightTestInfoSource() {
4486
+ try {
4487
+ const packageJson = resolvePlaywrightPackageJson();
4488
+ if (!packageJson) return () => null;
4489
+ const globals = (0, import_node_module2.createRequire)(packageJson)(path11.join(path11.dirname(packageJson), "lib", "globals.js"));
4490
+ const read = globals.currentTestInfo;
4491
+ if (typeof read !== "function") return () => null;
4492
+ return () => {
4493
+ try {
4494
+ return read() ?? null;
4495
+ } catch {
4496
+ return null;
4497
+ }
4498
+ };
4499
+ } catch {
4500
+ return () => null;
4501
+ }
4502
+ }
4503
+ function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
4504
+ return value === "fail" || value === "close" ? value : "report";
4505
+ }
4506
+ function countHandles() {
4507
+ const counts = {};
4508
+ const read = process.getActiveResourcesInfo;
4509
+ if (typeof read !== "function") return counts;
4510
+ for (const type of read.call(process)) {
4511
+ if (TRACKED_HANDLE_TYPES.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
4512
+ }
4513
+ return counts;
4514
+ }
4515
+ async function settledHandles(start) {
4516
+ const end = countHandles();
4517
+ if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
4518
+ await new Promise((resolve3) => setTimeout(resolve3, 0));
4519
+ return countHandles();
4520
+ }
4521
+ function settleWithin(promise, ms) {
4522
+ let timer;
4523
+ return Promise.race([
4524
+ promise.catch(() => void 0),
4525
+ new Promise((resolve3) => {
4526
+ timer = setTimeout(resolve3, ms);
4527
+ })
4528
+ ]).finally(() => clearTimeout(timer));
4529
+ }
4530
+ var ResourceLedger = class {
4531
+ constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}) {
4532
+ this.playwright = playwright;
4533
+ this.workerIndex = workerIndex;
4534
+ this.leakCheck = leakCheck;
4535
+ this.resultsFile = resultsFile;
4536
+ this.testInfoSource = testInfoSource;
4537
+ this.now = now;
4538
+ this.readers = readers;
4539
+ this.nextId = 1;
4540
+ this.entries = /* @__PURE__ */ new Map();
4541
+ this.byObject = /* @__PURE__ */ new WeakMap();
4542
+ this.pendingCalls = /* @__PURE__ */ new Set();
4543
+ this.lastCall = null;
4544
+ /** The page a wrapped page method is calling into: its first reported API call marks it used. */
4545
+ this.usingPage = null;
4546
+ this.born = [];
4547
+ this.closed = [];
4548
+ this.handlesAtStart = {};
4549
+ this.openAtStart = { contexts: 0, pages: 0 };
4550
+ /** The test between `testStarted` and `testEnded`, for when Playwright's own test info cannot be read. */
4551
+ this.window = null;
4552
+ this.listener = null;
4553
+ /** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
4554
+ this.sawBrowser = false;
4555
+ }
4556
+ /** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
4557
+ install() {
4558
+ const instrumentation = this.playwright?._instrumentation;
4559
+ if (!instrumentation || typeof instrumentation.addListener !== "function") return false;
4560
+ this.listener = {
4561
+ onApiCallBegin: (zone) => {
4562
+ if (this.pendingCalls.size > 1e3) this.pendingCalls.clear();
4563
+ this.pendingCalls.add(zone);
4564
+ this.lastCall = zone;
4565
+ if (this.usingPage) {
4566
+ this.usingPage.used = true;
4567
+ this.usingPage = null;
4568
+ }
4569
+ },
4570
+ onApiCallEnd: (zone) => {
4571
+ this.pendingCalls.delete(zone);
4572
+ },
4573
+ runAfterCreateBrowserContext: async (context) => {
4574
+ this.guard(() => this.onContext(context, userSite(new Error().stack)));
4575
+ },
4576
+ runAfterCreateRequestContext: async (request) => {
4577
+ this.guard(() => this.track(request, "request", null, userSite(new Error().stack)));
4578
+ },
4579
+ runBeforeCloseRequestContext: async (request) => {
4580
+ this.guard(() => this.onClosed(request));
4581
+ }
4582
+ };
4583
+ instrumentation.addListener(this.listener);
4584
+ for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
4585
+ if (type) this.wrapBrowserType(type);
4586
+ }
4587
+ return true;
4588
+ }
4589
+ /** Stop listening; the objects already tracked stay in the ledger. */
4590
+ uninstall() {
4591
+ const remove = this.playwright?._instrumentation?.removeListener;
4592
+ if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
4593
+ this.listener = null;
4594
+ }
4595
+ /** Mark a page used: a locator was built on it. */
4596
+ notePageUse(page) {
4597
+ const entry = this.byObject.get(page);
4598
+ if (entry) entry.used = true;
4599
+ }
4600
+ /** Open the census window of a test. */
4601
+ testStarted(info) {
4602
+ this.window = testRef2(info);
4603
+ this.handlesAtStart = countHandles();
4604
+ this.openAtStart = { contexts: 0, pages: 0 };
4605
+ for (const entry of this.openEntries()) {
4606
+ if (entry.birth.kind === "page") this.openAtStart.pages++;
4607
+ else if (entry.birth.kind === "context") this.openAtStart.contexts++;
4608
+ }
4609
+ this.readers.metrics?.start(this.sawBrowser);
4610
+ }
4611
+ /**
4612
+ * Close the census window of a test: apply `PIWI_LEAK_CHECK=close` to what the
4613
+ * test left open, then take the census. Called after the test-scoped fixtures
4614
+ * tore down, so what the test opened through them is closed by now.
4615
+ */
4616
+ async testEnded(info) {
4617
+ const test = testRef2(info) ?? this.window;
4618
+ const failed = typeof info.status === "string" && typeof info.expectedStatus === "string" && info.status !== info.expectedStatus;
4619
+ const leaks = test && !failed ? this.leaksOf(test.id) : [];
4620
+ if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
4621
+ const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
4622
+ const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
4623
+ const handles = { start: this.handlesAtStart, end: await settledHandles(this.handlesAtStart) };
4624
+ const census = this.census(ref, handles, await this.readPages());
4625
+ if (metrics) census.metrics = metrics;
4626
+ if (test) {
4627
+ census.openAtStart = this.openAtStart;
4628
+ if (!failed) census.leftOpen = leaks.length;
4629
+ }
4630
+ this.window = null;
4631
+ return { census, leaks };
4632
+ }
4633
+ /** The open pages' main thread and weight, over CDP, within a deadline. */
4634
+ async readPages() {
4635
+ const out = /* @__PURE__ */ new Map();
4636
+ const reader = this.readers.pages;
4637
+ if (!reader) return out;
4638
+ const pages = this.openEntries().filter((entry) => entry.birth.kind === "page").slice(0, PAGE_READ_CAP);
4639
+ await settleWithin(
4640
+ Promise.all(
4641
+ pages.map(async (entry) => {
4642
+ const main = entry.object ? await reader.read(entry.object) : null;
4643
+ if (main) out.set(entry.birth.id, main);
4644
+ })
4645
+ ),
4646
+ PAGE_READ_TIMEOUT_MS
4647
+ );
4648
+ return out;
4649
+ }
4650
+ /** The census a worker takes as it shuts down, appended to the results file when the reporter set one. */
4651
+ workerEnded() {
4652
+ const census = this.census(null);
4653
+ if (this.resultsFile) {
4654
+ try {
4655
+ fs8.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
4656
+ `);
4657
+ } catch {
4658
+ }
4659
+ }
4660
+ this.readers.metrics?.dispose();
4661
+ this.uninstall();
4662
+ return census;
4663
+ }
4664
+ guard(run) {
4665
+ try {
4666
+ run();
4667
+ } catch {
4668
+ }
4669
+ }
4670
+ wrapBrowserType(type) {
4671
+ for (const method of ["launch", "connect", "connectOverCDP"]) {
4672
+ const original = type[method];
4673
+ if (typeof original !== "function") continue;
4674
+ const bound = original.bind(type);
4675
+ type[method] = async (...args) => {
4676
+ const site = userSite(new Error().stack);
4677
+ const browser = await bound(...args);
4678
+ this.guard(() => this.onBrowser(browser, site));
4679
+ return browser;
4680
+ };
4681
+ }
4682
+ }
4683
+ track(object, kind, parent, site) {
4684
+ const existing = this.byObject.get(object);
4685
+ if (existing) return existing;
4686
+ const info = this.testInfoSource();
4687
+ const { phase, fixture } = info ? runnableOf(info) : fallbackRunnable(this.window);
4688
+ const birth = {
4689
+ id: this.nextId++,
4690
+ kind,
4691
+ parent: parent ? this.byObject.get(parent)?.birth.id ?? null : null,
4692
+ at: this.now(),
4693
+ phase,
4694
+ fixture,
4695
+ test: testRef2(info) ?? this.window,
4696
+ site
4697
+ };
4698
+ const entry = { birth, object, closed: null, used: false };
4699
+ if (kind === "browser" || kind === "context") this.sawBrowser = true;
4700
+ this.entries.set(birth.id, entry);
4701
+ this.byObject.set(object, entry);
4702
+ this.born.push(birth);
4703
+ return entry;
4704
+ }
4705
+ onBrowser(browser, site) {
4706
+ const known = this.byObject.get(browser);
4707
+ if (known) return known;
4708
+ const entry = this.track(browser, "browser", null, site);
4709
+ browser.on("disconnected", () => this.guard(() => this.onClosed(browser)));
4710
+ return entry;
4711
+ }
4712
+ onContext(context, site) {
4713
+ if (this.byObject.has(context)) return;
4714
+ const browser = context.browser();
4715
+ if (browser) this.onBrowser(browser, site);
4716
+ const entry = this.track(context, "context", browser, site);
4717
+ const zone = this.latestPendingCall();
4718
+ if (zone?.apiName?.endsWith(".newPage")) entry.birth.implicit = true;
4719
+ if (context._forReuse) entry.birth.reused = true;
4720
+ context.on("close", () => this.guard(() => this.onClosed(context)));
4721
+ context.on("page", (page) => this.guard(() => this.onPage(page, context)));
4722
+ for (const page of context.pages()) this.onPage(page, context, { site });
4723
+ }
4724
+ /** `initial` is set for a page that came with its context (a persistent context's), which is no popup. */
4725
+ onPage(page, context, initial) {
4726
+ if (this.byObject.has(page)) return;
4727
+ const zone = this.latestPendingCall();
4728
+ const viaNewPage = !!zone?.apiName?.endsWith(".newPage");
4729
+ const popup = !viaNewPage && !initial;
4730
+ const site = viaNewPage ? zoneSite(zone) : initial ? initial.site : zoneSite(this.lastCall);
4731
+ const entry = this.track(page, "page", context, site);
4732
+ if (popup) {
4733
+ const opener = page._opener;
4734
+ const openerEntry = opener ? this.byObject.get(opener) : void 0;
4735
+ if (openerEntry) entry.birth.opener = openerEntry.birth.id;
4736
+ if (this.lastCall?.apiName) entry.birth.trigger = this.lastCall.apiName;
4737
+ }
4738
+ const markUsed = () => {
4739
+ entry.used = true;
4740
+ };
4741
+ page.on("load", markUsed);
4742
+ page.on("domcontentloaded", markUsed);
4743
+ page.on("framenavigated", (frame) => {
4744
+ try {
4745
+ if (frame === page.mainFrame() && frame.url() !== "about:blank") entry.used = true;
4746
+ } catch {
4747
+ }
4748
+ });
4749
+ page.on("close", () => this.guard(() => this.onClosed(page)));
4750
+ const methods = page;
4751
+ for (const method of USE_METHODS) {
4752
+ const original = methods[method];
4753
+ if (typeof original !== "function") continue;
4754
+ const bound = original.bind(page);
4755
+ methods[method] = (...args) => {
4756
+ const previous = this.usingPage;
4757
+ this.usingPage = entry;
4758
+ try {
4759
+ return bound(...args);
4760
+ } finally {
4761
+ this.usingPage = previous;
4762
+ }
4763
+ };
4764
+ }
4765
+ }
4766
+ onClosed(object, byPiwi = false) {
4767
+ const entry = this.byObject.get(object);
4768
+ if (!entry || entry.closed) return;
4769
+ const info = this.testInfoSource();
4770
+ const { phase } = info ? runnableOf(info) : fallbackRunnable(this.window);
4771
+ const close = {
4772
+ id: entry.birth.id,
4773
+ at: this.now(),
4774
+ phase,
4775
+ test: testRef2(info) ?? this.window
4776
+ };
4777
+ if (entry.birth.kind === "page") close.used = entry.used;
4778
+ if (byPiwi) close.byPiwi = true;
4779
+ entry.closed = close;
4780
+ entry.object = null;
4781
+ this.closed.push(close);
4782
+ }
4783
+ latestPendingCall() {
4784
+ let latest = null;
4785
+ for (const zone of this.pendingCalls) latest = zone;
4786
+ return latest;
4787
+ }
4788
+ openEntries() {
4789
+ return [...this.entries.values()].filter((entry) => !entry.closed && entry.object);
4790
+ }
4791
+ /**
4792
+ * Objects a test opened itself, by its body or its each-hooks, and left open.
4793
+ * A page in a leaked context, or a context in a leaked browser, goes with it;
4794
+ * the context `browser.newPage()` opened for its page goes with the page.
4795
+ */
4796
+ leaksOf(testId) {
4797
+ const open = this.openEntries().filter(
4798
+ (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
4799
+ );
4800
+ const openIds = new Set(open.map((entry) => entry.birth.id));
4801
+ const foldsIntoLeak = (entry) => {
4802
+ let parentId = entry.birth.parent;
4803
+ while (parentId !== null) {
4804
+ if (!this.isImplicit(parentId)) return openIds.has(parentId);
4805
+ parentId = this.entries.get(parentId)?.birth.parent ?? null;
4806
+ }
4807
+ return false;
4808
+ };
4809
+ return open.filter((entry) => !(entry.birth.kind === "context" && this.isImplicit(entry.birth.id))).filter((entry) => !foldsIntoLeak(entry)).map((entry) => ({
4810
+ id: entry.birth.id,
4811
+ kind: entry.birth.kind,
4812
+ site: entry.birth.site,
4813
+ pages: entry.birth.kind === "page" ? 0 : this.pagesUnder(entry.birth.id),
4814
+ implicit: false,
4815
+ ...entry.birth.opener !== void 0 ? { trigger: entry.birth.trigger ?? "a click" } : {}
4816
+ })).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
4817
+ }
4818
+ /** Open pages under a context, or under any context of a browser. */
4819
+ pagesUnder(id) {
4820
+ let count = 0;
4821
+ for (const entry of this.openEntries()) {
4822
+ if (entry.birth.kind !== "page" || entry.birth.parent === null) continue;
4823
+ const context = this.entries.get(entry.birth.parent);
4824
+ if (entry.birth.parent === id || context?.birth.parent === id) count++;
4825
+ }
4826
+ return count;
4827
+ }
4828
+ isImplicit(id) {
4829
+ const entry = this.entries.get(id);
4830
+ if (!entry || entry.birth.kind !== "context") return false;
4831
+ if (entry.birth.implicit) return true;
4832
+ const owner = entry.object ? entry.object._ownerPage : void 0;
4833
+ if (owner) entry.birth.implicit = true;
4834
+ return !!owner;
4835
+ }
4836
+ openPagesOf(contextId) {
4837
+ let count = 0;
4838
+ for (const entry of this.openEntries())
4839
+ if (entry.birth.kind === "page" && entry.birth.parent === contextId) count++;
4840
+ return count;
4841
+ }
4842
+ async closeLeaks(leaks) {
4843
+ for (const leak of leaks) {
4844
+ const entry = this.entries.get(leak.id);
4845
+ const object = entry?.object;
4846
+ if (!entry || entry.closed || !object) continue;
4847
+ const close = leak.kind === "request" ? () => object.dispose() : () => object.close();
4848
+ await settleWithin(internalCall(object, close), CLOSE_TIMEOUT_MS);
4849
+ const closedMeanwhile = entry.closed;
4850
+ if (closedMeanwhile) closedMeanwhile.byPiwi = true;
4851
+ else this.onClosed(object, true);
4852
+ }
4853
+ }
4854
+ census(test, handles, mains = /* @__PURE__ */ new Map()) {
4855
+ const open = [];
4856
+ for (const entry of this.openEntries()) {
4857
+ const object = entry.object;
4858
+ if (entry.birth.kind === "page") {
4859
+ const main = mains.get(entry.birth.id);
4860
+ open.push({
4861
+ id: entry.birth.id,
4862
+ used: entry.used,
4863
+ url: this.pageUrl(object),
4864
+ listeners: this.listenerCount(object),
4865
+ routes: this.routeCount(object),
4866
+ ...main ? { main } : {}
4867
+ });
4868
+ } else if (entry.birth.kind === "context") {
4869
+ this.isImplicit(entry.birth.id);
4870
+ open.push({ id: entry.birth.id, pages: this.openPagesOf(entry.birth.id), routes: this.routeCount(object) });
4871
+ } else {
4872
+ open.push({ id: entry.birth.id });
4873
+ }
4874
+ }
4875
+ const census = {
4876
+ v: 1,
4877
+ worker: this.workerIndex,
4878
+ pid: process.pid,
4879
+ at: this.now(),
4880
+ test,
4881
+ born: this.born,
4882
+ closed: this.closed,
4883
+ open
4884
+ };
4885
+ if (handles) census.handles = handles;
4886
+ this.born = [];
4887
+ this.closed = [];
4888
+ return census;
4889
+ }
4890
+ pageUrl(page) {
4891
+ try {
4892
+ const url = typeof page.url === "function" ? String(page.url()) : "";
4893
+ return url ? url.split(/[?#]/)[0] : void 0;
4894
+ } catch {
4895
+ return void 0;
4896
+ }
4897
+ }
4898
+ listenerCount(emitter) {
4899
+ try {
4900
+ if (typeof emitter.eventNames !== "function" || typeof emitter.listenerCount !== "function") return void 0;
4901
+ const names = emitter.eventNames();
4902
+ return names.reduce((sum, name) => sum + emitter.listenerCount(name), 0);
4903
+ } catch {
4904
+ return void 0;
4905
+ }
4906
+ }
4907
+ routeCount(object) {
4908
+ const routes = object._routes;
4909
+ return Array.isArray(routes) ? routes.length : void 0;
4910
+ }
4911
+ };
4912
+ var activeLedger = null;
4913
+ function startResourceLedger(playwright, workerIndex) {
4914
+ if (process.env[PIWI_ENV_KEYS.captureResources] === "false") return null;
4915
+ if (activeLedger) return activeLedger;
4916
+ const metrics = new WorkerMetrics();
4917
+ const ledger = new ResourceLedger(
4918
+ playwright,
4919
+ workerIndex,
4920
+ readLeakCheck(),
4921
+ process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
4922
+ playwrightTestInfoSource(),
4923
+ Date.now,
4924
+ { metrics, pages: new PageReader() }
4925
+ );
4926
+ if (!ledger.install()) {
4927
+ metrics.dispose();
4928
+ return null;
4929
+ }
4930
+ activeLedger = ledger;
4931
+ return ledger;
4932
+ }
4933
+ function activeResourceLedger() {
4934
+ return activeLedger;
4935
+ }
4936
+ function stopResourceLedger() {
4937
+ const ledger = activeLedger;
4938
+ activeLedger = null;
4939
+ return ledger ? ledger.workerEnded() : null;
4940
+ }
4941
+ function noteResourceUse(page) {
4942
+ activeLedger?.notePageUse(page);
4943
+ }
4944
+ var ARTICLE = {
4945
+ browser: "a browser",
4946
+ context: "a context",
4947
+ page: "a page",
4948
+ request: "an API request context"
4949
+ };
4950
+ function leakFailureMessage(leaks) {
4951
+ const lines = leaks.map((leak) => {
4952
+ const what = leak.trigger ? `a popup opened by ${leak.trigger}` : ARTICLE[leak.kind];
4953
+ const where = leak.site ? `${leak.trigger ? "" : " opened"} at ${leak.site}` : "";
4954
+ const pages = leak.pages > 0 ? `, with ${leak.pages} page${leak.pages === 1 ? "" : "s"}` : "";
4955
+ return ` - ${what}${where}${pages}`;
4956
+ });
4957
+ return [
4958
+ `Piwi: this test left ${leaks.length === 1 ? "one object" : `${leaks.length} objects`} open:`,
4959
+ ...lines,
4960
+ "Close each before the test ends (await using, close() or dispose()), or set PIWI_LEAK_CHECK=report to only list them."
4961
+ ].join("\n");
4962
+ }
4963
+ async function recordResourceCensus(testInfo) {
4964
+ const ledger = activeLedger;
4965
+ if (!ledger) return;
4966
+ const { census, leaks } = await ledger.testEnded(testInfo);
4967
+ try {
4968
+ await testInfo.attach(ATTACHMENT_NAMES.resources, {
4969
+ contentType: "application/json",
4970
+ body: Buffer.from(JSON.stringify(census))
4971
+ });
4972
+ } catch {
4973
+ }
4974
+ if (ledger.leakCheck === "fail" && leaks.length > 0) throw new Error(leakFailureMessage(leaks));
4975
+ }
4976
+
3978
4977
  // src/internal/capture/capture-fixtures.ts
3979
4978
  function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
3980
4979
  const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
@@ -4047,7 +5046,7 @@ function configuredCodeReachRoots() {
4047
5046
  if (Array.isArray(parsed)) return parsed.filter((r) => typeof r === "string" && r !== "");
4048
5047
  } catch {
4049
5048
  }
4050
- return raw.split(path10.delimiter).filter(Boolean);
5049
+ return raw.split(path12.delimiter).filter(Boolean);
4051
5050
  }
4052
5051
  async function stopSinkCodeReach(sink, closing) {
4053
5052
  const session = sink.codeReach;
@@ -4062,7 +5061,7 @@ async function stopSinkCodeReach(sink, closing) {
4062
5061
  if (!entries) return;
4063
5062
  const info = sink.testInfo;
4064
5063
  const configFile = info?.config.configFile;
4065
- const configDir = configFile ? path10.dirname(configFile) : info?.config.rootDir ?? process.cwd();
5064
+ const configDir = configFile ? path12.dirname(configFile) : info?.config.rootDir ?? process.cwd();
4066
5065
  session.files = await resolveCodeReach(
4067
5066
  entries,
4068
5067
  codeReachRoots(configDir, configuredCodeReachRoots()),
@@ -4626,6 +5625,7 @@ function instrumentPage(page) {
4626
5625
  const original = factories[method].bind(page);
4627
5626
  factories[method] = (...args) => {
4628
5627
  if (currentSink) currentSink.lastActivePage = page;
5628
+ noteResourceUse(page);
4629
5629
  return wrapLocator(page, original(...args), method, args);
4630
5630
  };
4631
5631
  }
@@ -4955,6 +5955,22 @@ async function flushSink(sink, testInfo) {
4955
5955
  }
4956
5956
  }
4957
5957
  var piwiFixtures = {
5958
+ // Auto, worker-scoped: the worker's resource ledger. Set up before any hook
5959
+ // (worker fixtures come first), so a `beforeAll` context is in it too, and
5960
+ // torn down after the browser closed, so its census sees what only the
5961
+ // worker's shutdown closed. Depends on `playwright` alone, which launches nothing.
5962
+ piwiResources: [
5963
+ async ({ playwright }, use, workerInfo) => {
5964
+ boxCaptureFrames();
5965
+ startResourceLedger(playwright, workerInfo.workerIndex);
5966
+ try {
5967
+ await use();
5968
+ } finally {
5969
+ stopResourceLedger();
5970
+ }
5971
+ },
5972
+ { scope: "worker", auto: true }
5973
+ ],
4958
5974
  // Worker-scoped: patch the shared browser so every page/context created from
4959
5975
  // it — including by user fixtures that take `browser` directly — is captured.
4960
5976
  browser: [
@@ -5002,6 +6018,7 @@ var piwiFixtures = {
5002
6018
  file: testInfo.file,
5003
6019
  titlePath: testInfo.titlePath
5004
6020
  });
6021
+ activeResourceLedger()?.testStarted(testInfo);
5005
6022
  currentSink = sink;
5006
6023
  try {
5007
6024
  await use();
@@ -5028,6 +6045,7 @@ var piwiFixtures = {
5028
6045
  }
5029
6046
  if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
5030
6047
  await flushSink(sink, testInfo);
6048
+ await recordResourceCensus(testInfo);
5031
6049
  }
5032
6050
  },
5033
6051
  { auto: true }
@@ -5061,6 +6079,7 @@ function extendPiwiFixtures(test) {
5061
6079
  0 && (module.exports = {
5062
6080
  CAPTURED_ATTRS_ARG,
5063
6081
  LocatorPageLog,
6082
+ activeResourceLedger,
5064
6083
  ariaSnapshotBestEffort,
5065
6084
  ariaSnapshotJSONBestEffort,
5066
6085
  boxCaptureFrames,
@@ -5073,10 +6092,14 @@ function extendPiwiFixtures(test) {
5073
6092
  internalCall,
5074
6093
  noteLocatorCall,
5075
6094
  noteNavigation,
6095
+ noteResourceUse,
5076
6096
  pageMapFetcher,
5077
6097
  piwiFixtures,
5078
6098
  probeElementAttrs,
6099
+ recordResourceCensus,
5079
6100
  resolveCodeReach,
5080
6101
  startCodeReach,
5081
- stopCodeReach
6102
+ startResourceLedger,
6103
+ stopCodeReach,
6104
+ stopResourceLedger
5082
6105
  });