@piwitests/reporter 0.45.0 → 0.47.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.
@@ -63,6 +63,8 @@ var import_node_zlib2 = require("zlib");
63
63
  // ../picker-dom/src/probe.ts
64
64
  function probeElementAttrs(el, arg) {
65
65
  const { keep, tagRoles, inputRoles, roleSources, includeStructural } = arg;
66
+ const countMatches = arg.countMatches !== false;
67
+ const testIdAttr = arg.testIdAttribute || "data-testid";
66
68
  const FIELD_TAGS = ["input", "select", "textarea"];
67
69
  const textWithout = (root, skip) => {
68
70
  if (root === skip) return "";
@@ -87,42 +89,45 @@ function probeElementAttrs(el, arg) {
87
89
  }
88
90
  };
89
91
  const attrMap = {};
90
- 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) {
91
94
  const v = el.getAttribute(key) ?? el[key];
92
95
  attrMap[key] = typeof v === "string" ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
93
96
  }
94
97
  const r = el.getBoundingClientRect();
95
98
  const selectorCounts = {};
96
- try {
97
- const doc = el.ownerDocument;
98
- const cssEsc = (s) => doc.defaultView.CSS.escape(s);
99
- const count = (sel) => {
100
- try {
101
- return doc.querySelectorAll(sel).length;
102
- } catch {
103
- 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])}]`);
104
112
  }
105
- };
106
- if (attrMap["data-testid"]) {
107
- selectorCounts.testId = count(`[data-testid=${JSON.stringify(attrMap["data-testid"])}]`);
108
- }
109
- if (attrMap["id"]) selectorCounts.id = count(`#${cssEsc(attrMap["id"])}`);
110
- if (attrMap["name"]) selectorCounts.name = count(`[name=${JSON.stringify(attrMap["name"])}]`);
111
- if (attrMap["placeholder"]) {
112
- selectorCounts.placeholder = count(`[placeholder=${JSON.stringify(attrMap["placeholder"])}]`);
113
- }
114
- if (attrMap["alt"]) selectorCounts.alt = count(`[alt=${JSON.stringify(attrMap["alt"])}]`);
115
- if (attrMap["title"]) selectorCounts.title = count(`[title=${JSON.stringify(attrMap["title"])}]`);
116
- const classList = (attrMap["class"] || "").split(/\s+/).filter((c) => c.length > 1).slice(0, 10);
117
- if (classList.length > 0) {
118
- const classCounts = {};
119
- for (const cls of classList) {
120
- const n = count(`.${cssEsc(cls)}`);
121
- 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
+ }
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;
122
128
  }
123
- selectorCounts.classes = classCounts;
129
+ } catch {
124
130
  }
125
- } catch {
126
131
  }
127
132
  let rolePosition = null;
128
133
  const ancestors = [];
@@ -211,8 +216,8 @@ function probeElementAttrs(el, arg) {
211
216
  }
212
217
  return total;
213
218
  };
214
- const nodes = doc.querySelectorAll(roleSources);
215
- const nodesUsable = nodes.length <= 4e3;
219
+ const nodes = countMatches ? doc.querySelectorAll(roleSources) : [];
220
+ const nodesUsable = countMatches && nodes.length <= 4e3;
216
221
  const rolesUsable = !!targetRole && nodesUsable;
217
222
  if (rolesUsable) {
218
223
  let roleCountAll = 0;
@@ -246,17 +251,18 @@ function probeElementAttrs(el, arg) {
246
251
  };
247
252
  }
248
253
  }
249
- if (textNeedle) {
254
+ if (textNeedle && countMatches) {
250
255
  const textCount = countTextOwners(doc.body || doc.documentElement, 4e3);
251
256
  if (textCount >= 0) selectorCounts.text = textCount;
252
257
  }
253
- if (rolesUsable || textNeedle) {
258
+ if (rolesUsable || textNeedle || !countMatches) {
254
259
  const CONTAINER_TAGS = ["form", "nav", "main", "article", "section", "dialog", "table", "li", "tr"];
255
260
  const NOISY_DATA_ATTR = /^data-(v-[0-9a-f]+|reactid|react-checksum|svelte-\w+|ng-\w+|ember\w*)$/i;
256
261
  const POSITIONAL_DATA_ATTR = /^data-(index|idx|i|key|row|rownum|col|column|position|pos|order|sort|offset|page)$/i;
257
262
  const TEST_DATA_ATTRS2 = ["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"];
263
+ const testIdName = testIdAttr.toLowerCase();
258
264
  const usableDataAttr = (name, value) => {
259
- 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;
260
266
  if (NOISY_DATA_ATTR.test(name) || POSITIONAL_DATA_ATTR.test(name)) return false;
261
267
  return !!value && value.length <= 120;
262
268
  };
@@ -286,7 +292,13 @@ function probeElementAttrs(el, arg) {
286
292
  return digits <= letters;
287
293
  };
288
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
+ };
289
300
  const discriminatingText = (anc) => {
301
+ if (!countMatches && !hasAtMost(anc, 200)) return null;
290
302
  const heading = anc.querySelector('h1,h2,h3,h4,h5,h6,[role="heading"]');
291
303
  if (heading) {
292
304
  const t = rawText(heading);
@@ -318,7 +330,7 @@ function probeElementAttrs(el, arg) {
318
330
  depth++;
319
331
  const tag = (node.tagName || "").toLowerCase();
320
332
  if (tag === "body" || tag === "html") break;
321
- const testId = node.getAttribute("data-testid");
333
+ const testId = node.getAttribute(testIdAttr);
322
334
  const id = node.getAttribute("id");
323
335
  const explicitRole = node.getAttribute("role");
324
336
  const ariaLabel = node.getAttribute("aria-label");
@@ -338,10 +350,10 @@ function probeElementAttrs(el, arg) {
338
350
  }
339
351
  }
340
352
  }
341
- const scopedTextCount = countTextOwners(node, 2e3);
353
+ const scopedTextCount = countMatches ? countTextOwners(node, 2e3) : -1;
342
354
  let filterText = null;
343
355
  try {
344
- if (anchorRole && nodesUsable) filterText = discriminatingText(node);
356
+ if (anchorRole && (nodesUsable || !countMatches)) filterText = discriminatingText(node);
345
357
  } catch {
346
358
  filterText = null;
347
359
  }
@@ -354,11 +366,13 @@ function probeElementAttrs(el, arg) {
354
366
  ariaLabel: ariaLabel || null,
355
367
  ...scopedRoleCount >= 0 ? { scopedRoleCount } : {},
356
368
  ...scopedTextCount >= 0 ? { scopedTextCount } : {},
357
- ...testId ? { testIdCount: count(`[data-testid=${JSON.stringify(testId)}]`) } : {},
358
- ...id ? { idCount: count(`#${cssEsc(id)}`) } : {},
369
+ ...testId && countMatches ? { testIdCount: count(`[${testIdAttr}=${JSON.stringify(testId)}]`) } : {},
370
+ ...id && countMatches ? { idCount: count(`#${cssEsc(id)}`) } : {},
359
371
  ...anchorRole && nodesUsable ? { roleCount: docRoleCount(anchorRole) } : {},
360
- ...filterText ? { filterText, filterRoleCount: filterMatchCount(anchorRole, filterText) } : {},
361
- ...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)}]`) } : {}
362
376
  });
363
377
  }
364
378
  node = node.parentElement;
@@ -396,6 +410,17 @@ function installPickerOverlay(arg) {
396
410
  return;
397
411
  }
398
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
+ }
399
424
  const highlight = doc.createElement("div");
400
425
  highlight.id = "__piwi_picker_highlight";
401
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);`;
@@ -420,27 +445,53 @@ function installPickerOverlay(arg) {
420
445
  const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
421
446
  const hlLocator = (expr) => `<code style="font-family:${MONO}">${hlTokens(expr)}</code>`;
422
447
  const head = doc.createElement("div");
423
- 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;
424
452
  const locatorLine = doc.createElement("div");
425
453
  locatorLine.id = "__piwi_picker_locator";
426
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;`;
427
455
  const foot = doc.createElement("div");
428
456
  foot.id = "__piwi_picker_foot";
429
457
  foot.style.cssText = "color:#9ca3af;margin-top:6px;font-size:12px;";
430
- foot.textContent = "\u2191 parent \xB7 \u2193 child \xB7 Esc skip";
458
+ foot.textContent = words.keys;
431
459
  banner.appendChild(head);
432
460
  banner.appendChild(locatorLine);
433
461
  banner.appendChild(foot);
434
462
  const label = doc.createElement("div");
435
463
  label.id = "__piwi_picker_label";
436
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);`;
437
- doc.body.appendChild(highlight);
438
- doc.body.appendChild(label);
439
- 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);
440
491
  const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
441
492
  const describe = (el) => {
442
493
  const tag = (el.tagName || "?").toLowerCase();
443
- const testId = el.getAttribute && el.getAttribute("data-testid");
494
+ const testId = el.getAttribute && el.getAttribute(testIdAttr);
444
495
  if (testId) return `getByTestId('${escJs(testId)}')`;
445
496
  if (el.labels && el.labels.length > 0) {
446
497
  const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
@@ -475,7 +526,7 @@ function installPickerOverlay(arg) {
475
526
  const el = chain2[i];
476
527
  const tag = (el.tagName || "").toLowerCase();
477
528
  if (ACTIONABLE_TAGS.includes(tag)) return i;
478
- if (el.getAttribute && (el.getAttribute("role") || el.getAttribute("data-testid"))) return i;
529
+ if (el.getAttribute && (el.getAttribute("role") || el.getAttribute(testIdAttr))) return i;
479
530
  }
480
531
  return 0;
481
532
  };
@@ -531,7 +582,7 @@ function installPickerOverlay(arg) {
531
582
  locatorLine.style.display = "block";
532
583
  }
533
584
  placeLabel(r);
534
- foot.textContent = "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip";
585
+ foot.textContent = words.keysHovering;
535
586
  };
536
587
  const redescribe = () => {
537
588
  labeled = null;
@@ -594,6 +645,13 @@ function installPickerOverlay(arg) {
594
645
  banner.remove();
595
646
  if (g.__piwiRedescribe === redescribe) delete g.__piwiRedescribe;
596
647
  };
648
+ const teardown = () => {
649
+ removePickingListeners();
650
+ removeSuppressed();
651
+ doc.removeEventListener("click", stop, true);
652
+ doc.removeEventListener("keydown", stop, true);
653
+ cleanup();
654
+ };
597
655
  const reportPicked = (el) => {
598
656
  removePickingListeners();
599
657
  highlight.style.display = "none";
@@ -604,13 +662,13 @@ function installPickerOverlay(arg) {
604
662
  g.__piwiSnapshotExtras?.onPick?.();
605
663
  doc.addEventListener("click", stop, true);
606
664
  doc.addEventListener("keydown", stop, true);
607
- foot.textContent = "Analyzing element\u2026";
665
+ foot.textContent = words.analyzing;
608
666
  g.parent.postMessage({ type: "elementPicked", attrs }, "*");
609
667
  } else {
610
668
  removeSuppressed();
611
669
  g.__piwiPickedElement = el;
612
670
  g.__piwiPickState = "picked";
613
- foot.textContent = "Analyzing element\u2026";
671
+ foot.textContent = words.analyzing;
614
672
  }
615
673
  };
616
674
  const reportSkipped = () => {
@@ -658,7 +716,7 @@ function installPickerOverlay(arg) {
658
716
  }
659
717
  });
660
718
  };
661
- g.__piwiPickCleanup = cleanup;
719
+ g.__piwiPickCleanup = teardown;
662
720
  doc.addEventListener("mousemove", onMove, true);
663
721
  doc.addEventListener("click", onClick, true);
664
722
  doc.addEventListener("keydown", onKey, true);
@@ -680,6 +738,26 @@ function showAnchorPicker(arg) {
680
738
  }
681
739
  const Z = 2147483600;
682
740
  const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
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));
683
761
  const roleOf2 = (n) => {
684
762
  const explicit = n.getAttribute && n.getAttribute("role");
685
763
  if (explicit) return explicit;
@@ -695,9 +773,11 @@ function showAnchorPicker(arg) {
695
773
  const al = n.getAttribute && n.getAttribute("aria-level");
696
774
  return al && /^\d+$/.test(al) ? Number(al) : null;
697
775
  };
776
+ const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
698
777
  const leafMatches = (scope) => {
699
778
  try {
700
- 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;
701
781
  const nodes = scope.querySelectorAll(roleSources);
702
782
  if (nodes.length > 2e3) return -1;
703
783
  let matched = 0;
@@ -733,12 +813,12 @@ function showAnchorPicker(arg) {
733
813
  depth++;
734
814
  const tag = (node.tagName || "").toLowerCase();
735
815
  if (tag === "body" || tag === "html") break;
736
- const testId = node.getAttribute("data-testid");
816
+ const testId = node.getAttribute(testIdAttr);
737
817
  const id = node.getAttribute("id");
738
818
  const ariaLabel = node.getAttribute("aria-label");
739
819
  const role = roleOf2(node);
740
820
  const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
741
- if (testId) info.testIdCount = count(`[data-testid=${JSON.stringify(testId)}]`);
821
+ if (testId) info.testIdCount = count(`[${testIdAttr}=${JSON.stringify(testId)}]`);
742
822
  if (id) {
743
823
  try {
744
824
  info.idCount = count(`#${doc.defaultView.CSS.escape(id)}`);
@@ -757,7 +837,7 @@ function showAnchorPicker(arg) {
757
837
  if (ariaLabel) info.labeledRoleCount = labeledCount;
758
838
  }
759
839
  info.scopedLeafCount = leafMatches(node);
760
- 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;
761
841
  rows.push({
762
842
  node,
763
843
  info,
@@ -779,13 +859,13 @@ function showAnchorPicker(arg) {
779
859
  const pr = el.getBoundingClientRect();
780
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;`;
781
861
  const panel = doc.createElement("div");
782
- 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);`;
783
863
  const title = doc.createElement("div");
784
864
  title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
785
- title.textContent = "Scope to stable parents (optional)";
865
+ title.textContent = words.title;
786
866
  const sub = doc.createElement("div");
787
867
  sub.style.cssText = "color:#9ca3af;margin-bottom:10px;";
788
- sub.textContent = "Pick one or more parents to anchor the locator to. Hover a row to see the parent.";
868
+ sub.textContent = words.hint;
789
869
  panel.appendChild(title);
790
870
  panel.appendChild(sub);
791
871
  const selected = /* @__PURE__ */ new Set();
@@ -793,7 +873,7 @@ function showAnchorPicker(arg) {
793
873
  footer.style.cssText = "margin:10px 0;font-weight:600;";
794
874
  const segMatches = (scope, info) => {
795
875
  try {
796
- 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)}]`));
797
877
  if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
798
878
  const nodes = Array.from(scope.querySelectorAll(roleSources));
799
879
  if (nodes.length > 2e3) return [];
@@ -817,14 +897,15 @@ function showAnchorPicker(arg) {
817
897
  let total = 0;
818
898
  for (const s of scopes) {
819
899
  const c = leafMatches(s);
820
- if (c > 0) total += c;
900
+ if (c < 0) return -1;
901
+ total += c;
821
902
  if (total > 50) return total;
822
903
  }
823
904
  return total;
824
905
  };
825
906
  const refreshFooter = () => {
826
907
  if (selected.size === 0) {
827
- footer.textContent = "No parents selected \u2014 standard alternatives only.";
908
+ footer.textContent = words.noneSelected;
828
909
  footer.style.color = "#9ca3af";
829
910
  g.__piwiPickChainCount = void 0;
830
911
  return;
@@ -832,10 +913,10 @@ function showAnchorPicker(arg) {
832
913
  const c = chainCount();
833
914
  g.__piwiPickChainCount = c;
834
915
  if (c === 1) {
835
- footer.textContent = "\u2713 Selection matches exactly 1 element";
916
+ footer.textContent = words.matchesOne;
836
917
  footer.style.color = "#4ade80";
837
918
  } else {
838
- footer.textContent = c < 0 ? "Match count unavailable" : `\u2717 Selection matches ${c} elements`;
919
+ footer.textContent = c < 0 ? words.countUnavailable : counted(words.matchesMany, c);
839
920
  footer.style.color = "#fbbf24";
840
921
  }
841
922
  };
@@ -852,7 +933,8 @@ function showAnchorPicker(arg) {
852
933
  code.textContent = `<${row.info.tag}> ${row.hookLabel}`;
853
934
  const hint = doc.createElement("span");
854
935
  hint.style.cssText = "color:#9ca3af;";
855
- 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;
856
938
  text.appendChild(code);
857
939
  text.appendChild(hint);
858
940
  line.appendChild(box);
@@ -890,6 +972,7 @@ function showAnchorPicker(arg) {
890
972
  outline.remove();
891
973
  outlineLabel.remove();
892
974
  pickedOutline.remove();
975
+ if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
893
976
  };
894
977
  const done = (state) => {
895
978
  g.__piwiPickAnchors = state === "done" ? rows.filter((_, i) => selected.has(i)).map((r) => r.info) : [];
@@ -906,7 +989,7 @@ function showAnchorPicker(arg) {
906
989
  buttonRow.style.cssText = "display:flex;gap:8px;margin-top:4px;";
907
990
  const useBtn = doc.createElement("button");
908
991
  useBtn.style.cssText = "flex:1;background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer;font:600 12px system-ui;";
909
- useBtn.textContent = "Use selected parents";
992
+ useBtn.textContent = words.use;
910
993
  useBtn.addEventListener("click", (e) => {
911
994
  e.preventDefault();
912
995
  e.stopImmediatePropagation();
@@ -914,7 +997,7 @@ function showAnchorPicker(arg) {
914
997
  });
915
998
  const skipBtn = doc.createElement("button");
916
999
  skipBtn.style.cssText = "background:none;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:8px 10px;cursor:pointer;font:12px system-ui;";
917
- skipBtn.textContent = "Skip (Esc)";
1000
+ skipBtn.textContent = words.skip;
918
1001
  skipBtn.addEventListener("click", (e) => {
919
1002
  e.preventDefault();
920
1003
  e.stopImmediatePropagation();
@@ -926,10 +1009,32 @@ function showAnchorPicker(arg) {
926
1009
  refreshFooter();
927
1010
  g.__piwiAnchorCleanup = cleanup;
928
1011
  doc.addEventListener("keydown", onKey, true);
929
- doc.body.appendChild(pickedOutline);
930
- doc.body.appendChild(outline);
931
- doc.body.appendChild(outlineLabel);
932
- 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);
933
1038
  }
934
1039
 
935
1040
  // ../picker-dom/src/overlay-confirm.ts
@@ -1145,7 +1250,7 @@ function isAutoGenerated(value) {
1145
1250
  return false;
1146
1251
  }
1147
1252
  var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
1148
- function generateAlternatives(attrs) {
1253
+ function generateAlternatives(attrs, options = {}) {
1149
1254
  const alts = [];
1150
1255
  const seen = /* @__PURE__ */ new Set();
1151
1256
  const add = (loc) => {
@@ -1156,12 +1261,14 @@ function generateAlternatives(attrs) {
1156
1261
  };
1157
1262
  const { accessibleName } = attrs;
1158
1263
  const tag = attrs.tagName;
1159
- const role = resolveAriaRole(attrs);
1264
+ const role = options.role !== void 0 ? options.role : resolveAriaRole(attrs);
1160
1265
  const counts = attrs.selectorCounts;
1161
1266
  const isUnique = (n) => n == null || n <= 1;
1162
1267
  const ambiguityPenalty = (n) => n != null && n > 1 ? 45 : 0;
1163
1268
  const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
1164
- 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);
1165
1272
  if (testId && isUnique(counts?.testId)) {
1166
1273
  add({
1167
1274
  locator: `getByTestId('${esc(testId)}')`,
@@ -1220,7 +1327,7 @@ function generateAlternatives(attrs) {
1220
1327
  });
1221
1328
  }
1222
1329
  const id = attr(attrs, "id");
1223
- if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
1330
+ if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
1224
1331
  const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
1225
1332
  add({
1226
1333
  locator: `locator('${esc(selector)}')`,
@@ -1230,7 +1337,7 @@ function generateAlternatives(attrs) {
1230
1337
  });
1231
1338
  }
1232
1339
  const name = attr(attrs, "name");
1233
- if (name && isUnique(counts?.name)) {
1340
+ if (name && selectable("name") && isUnique(counts?.name)) {
1234
1341
  const selector = `[name="${escCssAttrValue(name)}"]`;
1235
1342
  add({
1236
1343
  locator: `locator('${esc(selector)}')`,
@@ -1275,7 +1382,7 @@ function generateAlternatives(attrs) {
1275
1382
  score: scores.testId
1276
1383
  });
1277
1384
  }
1278
- if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
1385
+ if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
1279
1386
  idAnchorDone = true;
1280
1387
  const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
1281
1388
  add({
@@ -1285,7 +1392,7 @@ function generateAlternatives(attrs) {
1285
1392
  score: scores.id
1286
1393
  });
1287
1394
  }
1288
- if (!dataAnchorDone && anc.dataAttr && anc.dataAttrCount === 1) {
1395
+ if (!dataAnchorDone && anc.dataAttr && selectable(anc.dataAttr.name) && anc.dataAttrCount === 1) {
1289
1396
  dataAnchorDone = true;
1290
1397
  const { name: name2, value } = anc.dataAttr;
1291
1398
  const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
@@ -2049,6 +2156,7 @@ var DEFAULTS = {
2049
2156
  sampleAriaOnPass: true,
2050
2157
  uploadManifest: true,
2051
2158
  defaultCapture: true,
2159
+ checkBaseUrl: false,
2052
2160
  streaming: true,
2053
2161
  streamingBatchSize: 5,
2054
2162
  streamingBatchDelay: 2e3,
@@ -2091,6 +2199,7 @@ var PIWI_ENV_KEYS = {
2091
2199
  sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
2092
2200
  uploadManifest: "PIWI_UPLOAD_MANIFEST",
2093
2201
  defaultCapture: "PIWI_DEFAULT_CAPTURE",
2202
+ checkBaseUrl: "PIWI_CHECK_BASE_URL",
2094
2203
  inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
2095
2204
  pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
2096
2205
  outputFile: "PIWI_OUTPUT_FILE",
@@ -2143,6 +2252,7 @@ var ENV_FALLBACK_SPECS = [
2143
2252
  { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
2144
2253
  { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
2145
2254
  { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
2255
+ { option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
2146
2256
  { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
2147
2257
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
2148
2258
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
@@ -264,7 +264,7 @@ function isAutoGenerated(value) {
264
264
  return false;
265
265
  }
266
266
  var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
267
- function generateAlternatives(attrs) {
267
+ function generateAlternatives(attrs, options = {}) {
268
268
  const alts = [];
269
269
  const seen = /* @__PURE__ */ new Set();
270
270
  const add = (loc) => {
@@ -275,12 +275,14 @@ function generateAlternatives(attrs) {
275
275
  };
276
276
  const { accessibleName } = attrs;
277
277
  const tag = attrs.tagName;
278
- const role = resolveAriaRole(attrs);
278
+ const role = options.role !== void 0 ? options.role : resolveAriaRole(attrs);
279
279
  const counts = attrs.selectorCounts;
280
280
  const isUnique = (n) => n == null || n <= 1;
281
281
  const ambiguityPenalty = (n) => n != null && n > 1 ? 45 : 0;
282
282
  const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
283
- const testId = attr(attrs, "data-testid");
283
+ const testIdAttribute = options.testIdAttribute || "data-testid";
284
+ const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
285
+ const testId = attr(attrs, testIdAttribute);
284
286
  if (testId && isUnique(counts?.testId)) {
285
287
  add({
286
288
  locator: `getByTestId('${esc(testId)}')`,
@@ -339,7 +341,7 @@ function generateAlternatives(attrs) {
339
341
  });
340
342
  }
341
343
  const id = attr(attrs, "id");
342
- if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
344
+ if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
343
345
  const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
344
346
  add({
345
347
  locator: `locator('${esc(selector)}')`,
@@ -349,7 +351,7 @@ function generateAlternatives(attrs) {
349
351
  });
350
352
  }
351
353
  const name = attr(attrs, "name");
352
- if (name && isUnique(counts?.name)) {
354
+ if (name && selectable("name") && isUnique(counts?.name)) {
353
355
  const selector = `[name="${escCssAttrValue(name)}"]`;
354
356
  add({
355
357
  locator: `locator('${esc(selector)}')`,
@@ -394,7 +396,7 @@ function generateAlternatives(attrs) {
394
396
  score: scores.testId
395
397
  });
396
398
  }
397
- if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
399
+ if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
398
400
  idAnchorDone = true;
399
401
  const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
400
402
  add({
@@ -404,7 +406,7 @@ function generateAlternatives(attrs) {
404
406
  score: scores.id
405
407
  });
406
408
  }
407
- if (!dataAnchorDone && anc.dataAttr && anc.dataAttrCount === 1) {
409
+ if (!dataAnchorDone && anc.dataAttr && selectable(anc.dataAttr.name) && anc.dataAttrCount === 1) {
408
410
  dataAnchorDone = true;
409
411
  const { name: name2, value } = anc.dataAttr;
410
412
  const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;