@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.
- package/dist/cli/index.js +530 -249
- package/dist/global-setup-module.js +132 -0
- package/dist/index.d.ts +61 -1
- package/dist/index.js +693 -104
- package/dist/internal/capture/capture-fixtures.js +187 -77
- package/dist/internal/capture/locator-healing.js +9 -7
- package/dist/internal/capture/pick-on-failure.js +130 -37
- package/package.json +1 -1
- package/templates/skills/apply-locator-healing/SKILL.md +2 -0
- package/templates/skills/investigate-failure/SKILL.md +5 -1
- package/templates/skills/write-the-missing-test/SKILL.md +9 -2
|
@@ -335,6 +335,17 @@ function installPickerOverlay(arg) {
|
|
|
335
335
|
return;
|
|
336
336
|
}
|
|
337
337
|
const Z = 2147483600;
|
|
338
|
+
const testIdAttr = arg.testIdAttribute || "data-testid";
|
|
339
|
+
const words = {
|
|
340
|
+
banner: "Piwi inspector \u2014 click any element to generate locators for it",
|
|
341
|
+
keys: "\u2191 parent \xB7 \u2193 child \xB7 Esc skip",
|
|
342
|
+
keysHovering: "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip",
|
|
343
|
+
analyzing: "Analyzing element\u2026"
|
|
344
|
+
};
|
|
345
|
+
for (const key of Object.keys(words)) {
|
|
346
|
+
const given = arg.strings?.[key];
|
|
347
|
+
if (typeof given === "string" && given) words[key] = given;
|
|
348
|
+
}
|
|
338
349
|
const highlight = doc.createElement("div");
|
|
339
350
|
highlight.id = "__piwi_picker_highlight";
|
|
340
351
|
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);`;
|
|
@@ -359,27 +370,53 @@ function installPickerOverlay(arg) {
|
|
|
359
370
|
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
360
371
|
const hlLocator = (expr) => `<code style="font-family:${MONO}">${hlTokens(expr)}</code>`;
|
|
361
372
|
const head = doc.createElement("div");
|
|
362
|
-
|
|
373
|
+
if (arg.transport === "postMessage") head.textContent = "Click an element to generate locators";
|
|
374
|
+
else if (arg.failing)
|
|
375
|
+
head.innerHTML = `Piwi locator picker \u2014 click the element that should replace ${hlLocator(arg.failing)}`;
|
|
376
|
+
else head.textContent = words.banner;
|
|
363
377
|
const locatorLine = doc.createElement("div");
|
|
364
378
|
locatorLine.id = "__piwi_picker_locator";
|
|
365
379
|
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;`;
|
|
366
380
|
const foot = doc.createElement("div");
|
|
367
381
|
foot.id = "__piwi_picker_foot";
|
|
368
382
|
foot.style.cssText = "color:#9ca3af;margin-top:6px;font-size:12px;";
|
|
369
|
-
foot.textContent =
|
|
383
|
+
foot.textContent = words.keys;
|
|
370
384
|
banner.appendChild(head);
|
|
371
385
|
banner.appendChild(locatorLine);
|
|
372
386
|
banner.appendChild(foot);
|
|
373
387
|
const label = doc.createElement("div");
|
|
374
388
|
label.id = "__piwi_picker_label";
|
|
375
389
|
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);`;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
390
|
+
const mountParent = () => {
|
|
391
|
+
let modal = null;
|
|
392
|
+
try {
|
|
393
|
+
modal = doc.querySelector("dialog:modal");
|
|
394
|
+
} catch {
|
|
395
|
+
modal = null;
|
|
396
|
+
}
|
|
397
|
+
if (!modal) return doc.body;
|
|
398
|
+
const s = g.getComputedStyle(modal);
|
|
399
|
+
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);
|
|
400
|
+
return holdsFixed ? doc.body : modal;
|
|
401
|
+
};
|
|
402
|
+
const mount = (...nodes) => {
|
|
403
|
+
const parent = mountParent();
|
|
404
|
+
for (const node of nodes) parent.appendChild(node);
|
|
405
|
+
if (parent !== doc.body) {
|
|
406
|
+
parent.addEventListener(
|
|
407
|
+
"close",
|
|
408
|
+
() => {
|
|
409
|
+
for (const node of nodes) if (node.parentNode === parent) doc.body.appendChild(node);
|
|
410
|
+
},
|
|
411
|
+
{ once: true }
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
mount(highlight, label, banner);
|
|
379
416
|
const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
380
417
|
const describe = (el) => {
|
|
381
418
|
const tag = (el.tagName || "?").toLowerCase();
|
|
382
|
-
const testId = el.getAttribute && el.getAttribute(
|
|
419
|
+
const testId = el.getAttribute && el.getAttribute(testIdAttr);
|
|
383
420
|
if (testId) return `getByTestId('${escJs(testId)}')`;
|
|
384
421
|
if (el.labels && el.labels.length > 0) {
|
|
385
422
|
const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
@@ -414,7 +451,7 @@ function installPickerOverlay(arg) {
|
|
|
414
451
|
const el = chain2[i];
|
|
415
452
|
const tag = (el.tagName || "").toLowerCase();
|
|
416
453
|
if (ACTIONABLE_TAGS.includes(tag)) return i;
|
|
417
|
-
if (el.getAttribute && (el.getAttribute("role") || el.getAttribute(
|
|
454
|
+
if (el.getAttribute && (el.getAttribute("role") || el.getAttribute(testIdAttr))) return i;
|
|
418
455
|
}
|
|
419
456
|
return 0;
|
|
420
457
|
};
|
|
@@ -470,7 +507,7 @@ function installPickerOverlay(arg) {
|
|
|
470
507
|
locatorLine.style.display = "block";
|
|
471
508
|
}
|
|
472
509
|
placeLabel(r);
|
|
473
|
-
foot.textContent =
|
|
510
|
+
foot.textContent = words.keysHovering;
|
|
474
511
|
};
|
|
475
512
|
const redescribe = () => {
|
|
476
513
|
labeled = null;
|
|
@@ -533,6 +570,13 @@ function installPickerOverlay(arg) {
|
|
|
533
570
|
banner.remove();
|
|
534
571
|
if (g.__piwiRedescribe === redescribe) delete g.__piwiRedescribe;
|
|
535
572
|
};
|
|
573
|
+
const teardown = () => {
|
|
574
|
+
removePickingListeners();
|
|
575
|
+
removeSuppressed();
|
|
576
|
+
doc.removeEventListener("click", stop, true);
|
|
577
|
+
doc.removeEventListener("keydown", stop, true);
|
|
578
|
+
cleanup();
|
|
579
|
+
};
|
|
536
580
|
const reportPicked = (el) => {
|
|
537
581
|
removePickingListeners();
|
|
538
582
|
highlight.style.display = "none";
|
|
@@ -543,13 +587,13 @@ function installPickerOverlay(arg) {
|
|
|
543
587
|
g.__piwiSnapshotExtras?.onPick?.();
|
|
544
588
|
doc.addEventListener("click", stop, true);
|
|
545
589
|
doc.addEventListener("keydown", stop, true);
|
|
546
|
-
foot.textContent =
|
|
590
|
+
foot.textContent = words.analyzing;
|
|
547
591
|
g.parent.postMessage({ type: "elementPicked", attrs }, "*");
|
|
548
592
|
} else {
|
|
549
593
|
removeSuppressed();
|
|
550
594
|
g.__piwiPickedElement = el;
|
|
551
595
|
g.__piwiPickState = "picked";
|
|
552
|
-
foot.textContent =
|
|
596
|
+
foot.textContent = words.analyzing;
|
|
553
597
|
}
|
|
554
598
|
};
|
|
555
599
|
const reportSkipped = () => {
|
|
@@ -597,7 +641,7 @@ function installPickerOverlay(arg) {
|
|
|
597
641
|
}
|
|
598
642
|
});
|
|
599
643
|
};
|
|
600
|
-
g.__piwiPickCleanup =
|
|
644
|
+
g.__piwiPickCleanup = teardown;
|
|
601
645
|
doc.addEventListener("mousemove", onMove, true);
|
|
602
646
|
doc.addEventListener("click", onClick, true);
|
|
603
647
|
doc.addEventListener("keydown", onKey, true);
|
|
@@ -619,6 +663,26 @@ function showAnchorPicker(arg) {
|
|
|
619
663
|
}
|
|
620
664
|
const Z = 2147483600;
|
|
621
665
|
const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
|
|
666
|
+
const testIdAttr = arg.testIdAttribute || "data-testid";
|
|
667
|
+
const words = {
|
|
668
|
+
title: "Scope to stable parents (optional)",
|
|
669
|
+
hint: "Pick one or more parents to anchor the locator to. Hover a row to see the parent.",
|
|
670
|
+
noneSelected: "No parents selected \u2014 standard alternatives only.",
|
|
671
|
+
matchesOne: "\u2713 Selection matches exactly 1 element",
|
|
672
|
+
matchesMany: "\u2717 Selection matches {count} elements",
|
|
673
|
+
countUnavailable: "Match count unavailable",
|
|
674
|
+
containsOne: "contains exactly 1 matching element",
|
|
675
|
+
containsMany: "contains {count} matching elements",
|
|
676
|
+
needsTestId: `add a ${testIdAttr} to make this usable`,
|
|
677
|
+
noHook: "no stable hook",
|
|
678
|
+
use: "Use selected parents",
|
|
679
|
+
skip: "Skip (Esc)"
|
|
680
|
+
};
|
|
681
|
+
for (const key of Object.keys(words)) {
|
|
682
|
+
const given = arg.strings?.[key];
|
|
683
|
+
if (typeof given === "string" && given) words[key] = given;
|
|
684
|
+
}
|
|
685
|
+
const counted = (template, n) => template.replace("{count}", String(n));
|
|
622
686
|
const roleOf = (n) => {
|
|
623
687
|
const explicit = n.getAttribute && n.getAttribute("role");
|
|
624
688
|
if (explicit) return explicit;
|
|
@@ -634,9 +698,11 @@ function showAnchorPicker(arg) {
|
|
|
634
698
|
const al = n.getAttribute && n.getAttribute("aria-level");
|
|
635
699
|
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
636
700
|
};
|
|
701
|
+
const leafCountable = !!leafTestId || roleOf(el) === leafRole;
|
|
637
702
|
const leafMatches = (scope) => {
|
|
638
703
|
try {
|
|
639
|
-
if (leafTestId) return scope.querySelectorAll(`[
|
|
704
|
+
if (leafTestId) return scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(leafTestId)}]`).length;
|
|
705
|
+
if (!leafCountable) return -1;
|
|
640
706
|
const nodes = scope.querySelectorAll(roleSources);
|
|
641
707
|
if (nodes.length > 2e3) return -1;
|
|
642
708
|
let matched = 0;
|
|
@@ -672,12 +738,12 @@ function showAnchorPicker(arg) {
|
|
|
672
738
|
depth++;
|
|
673
739
|
const tag = (node.tagName || "").toLowerCase();
|
|
674
740
|
if (tag === "body" || tag === "html") break;
|
|
675
|
-
const testId = node.getAttribute(
|
|
741
|
+
const testId = node.getAttribute(testIdAttr);
|
|
676
742
|
const id = node.getAttribute("id");
|
|
677
743
|
const ariaLabel = node.getAttribute("aria-label");
|
|
678
744
|
const role = roleOf(node);
|
|
679
745
|
const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
|
|
680
|
-
if (testId) info.testIdCount = count(`[
|
|
746
|
+
if (testId) info.testIdCount = count(`[${testIdAttr}=${JSON.stringify(testId)}]`);
|
|
681
747
|
if (id) {
|
|
682
748
|
try {
|
|
683
749
|
info.idCount = count(`#${doc.defaultView.CSS.escape(id)}`);
|
|
@@ -696,7 +762,7 @@ function showAnchorPicker(arg) {
|
|
|
696
762
|
if (ariaLabel) info.labeledRoleCount = labeledCount;
|
|
697
763
|
}
|
|
698
764
|
info.scopedLeafCount = leafMatches(node);
|
|
699
|
-
const hookLabel = testId ?
|
|
765
|
+
const hookLabel = testId ? `${testIdAttr}="${testId}"` : id ? `#${id}` : ariaLabel && role ? `${role} "${ariaLabel}"` : role ? `role ${role}` : words.noHook;
|
|
700
766
|
rows.push({
|
|
701
767
|
node,
|
|
702
768
|
info,
|
|
@@ -718,13 +784,13 @@ function showAnchorPicker(arg) {
|
|
|
718
784
|
const pr = el.getBoundingClientRect();
|
|
719
785
|
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;`;
|
|
720
786
|
const panel = doc.createElement("div");
|
|
721
|
-
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);`;
|
|
787
|
+
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);`;
|
|
722
788
|
const title = doc.createElement("div");
|
|
723
789
|
title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
|
|
724
|
-
title.textContent =
|
|
790
|
+
title.textContent = words.title;
|
|
725
791
|
const sub = doc.createElement("div");
|
|
726
792
|
sub.style.cssText = "color:#9ca3af;margin-bottom:10px;";
|
|
727
|
-
sub.textContent =
|
|
793
|
+
sub.textContent = words.hint;
|
|
728
794
|
panel.appendChild(title);
|
|
729
795
|
panel.appendChild(sub);
|
|
730
796
|
const selected = /* @__PURE__ */ new Set();
|
|
@@ -732,7 +798,7 @@ function showAnchorPicker(arg) {
|
|
|
732
798
|
footer.style.cssText = "margin:10px 0;font-weight:600;";
|
|
733
799
|
const segMatches = (scope, info) => {
|
|
734
800
|
try {
|
|
735
|
-
if (info.testId) return Array.from(scope.querySelectorAll(`[
|
|
801
|
+
if (info.testId) return Array.from(scope.querySelectorAll(`[${testIdAttr}=${JSON.stringify(info.testId)}]`));
|
|
736
802
|
if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
|
|
737
803
|
const nodes = Array.from(scope.querySelectorAll(roleSources));
|
|
738
804
|
if (nodes.length > 2e3) return [];
|
|
@@ -756,14 +822,15 @@ function showAnchorPicker(arg) {
|
|
|
756
822
|
let total = 0;
|
|
757
823
|
for (const s of scopes) {
|
|
758
824
|
const c = leafMatches(s);
|
|
759
|
-
if (c
|
|
825
|
+
if (c < 0) return -1;
|
|
826
|
+
total += c;
|
|
760
827
|
if (total > 50) return total;
|
|
761
828
|
}
|
|
762
829
|
return total;
|
|
763
830
|
};
|
|
764
831
|
const refreshFooter = () => {
|
|
765
832
|
if (selected.size === 0) {
|
|
766
|
-
footer.textContent =
|
|
833
|
+
footer.textContent = words.noneSelected;
|
|
767
834
|
footer.style.color = "#9ca3af";
|
|
768
835
|
g.__piwiPickChainCount = void 0;
|
|
769
836
|
return;
|
|
@@ -771,10 +838,10 @@ function showAnchorPicker(arg) {
|
|
|
771
838
|
const c = chainCount();
|
|
772
839
|
g.__piwiPickChainCount = c;
|
|
773
840
|
if (c === 1) {
|
|
774
|
-
footer.textContent =
|
|
841
|
+
footer.textContent = words.matchesOne;
|
|
775
842
|
footer.style.color = "#4ade80";
|
|
776
843
|
} else {
|
|
777
|
-
footer.textContent = c < 0 ?
|
|
844
|
+
footer.textContent = c < 0 ? words.countUnavailable : counted(words.matchesMany, c);
|
|
778
845
|
footer.style.color = "#fbbf24";
|
|
779
846
|
}
|
|
780
847
|
};
|
|
@@ -791,7 +858,8 @@ function showAnchorPicker(arg) {
|
|
|
791
858
|
code.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
792
859
|
const hint = doc.createElement("span");
|
|
793
860
|
hint.style.cssText = "color:#9ca3af;";
|
|
794
|
-
|
|
861
|
+
const inside = row.info.scopedLeafCount ?? -1;
|
|
862
|
+
hint.textContent = row.selectable ? inside === 1 ? words.containsOne : inside < 0 ? words.countUnavailable : counted(words.containsMany, inside) : words.needsTestId;
|
|
795
863
|
text.appendChild(code);
|
|
796
864
|
text.appendChild(hint);
|
|
797
865
|
line.appendChild(box);
|
|
@@ -829,6 +897,7 @@ function showAnchorPicker(arg) {
|
|
|
829
897
|
outline.remove();
|
|
830
898
|
outlineLabel.remove();
|
|
831
899
|
pickedOutline.remove();
|
|
900
|
+
if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
|
|
832
901
|
};
|
|
833
902
|
const done = (state) => {
|
|
834
903
|
g.__piwiPickAnchors = state === "done" ? rows.filter((_, i) => selected.has(i)).map((r) => r.info) : [];
|
|
@@ -845,7 +914,7 @@ function showAnchorPicker(arg) {
|
|
|
845
914
|
buttonRow.style.cssText = "display:flex;gap:8px;margin-top:4px;";
|
|
846
915
|
const useBtn = doc.createElement("button");
|
|
847
916
|
useBtn.style.cssText = "flex:1;background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer;font:600 12px system-ui;";
|
|
848
|
-
useBtn.textContent =
|
|
917
|
+
useBtn.textContent = words.use;
|
|
849
918
|
useBtn.addEventListener("click", (e) => {
|
|
850
919
|
e.preventDefault();
|
|
851
920
|
e.stopImmediatePropagation();
|
|
@@ -853,7 +922,7 @@ function showAnchorPicker(arg) {
|
|
|
853
922
|
});
|
|
854
923
|
const skipBtn = doc.createElement("button");
|
|
855
924
|
skipBtn.style.cssText = "background:none;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:8px 10px;cursor:pointer;font:12px system-ui;";
|
|
856
|
-
skipBtn.textContent =
|
|
925
|
+
skipBtn.textContent = words.skip;
|
|
857
926
|
skipBtn.addEventListener("click", (e) => {
|
|
858
927
|
e.preventDefault();
|
|
859
928
|
e.stopImmediatePropagation();
|
|
@@ -865,10 +934,32 @@ function showAnchorPicker(arg) {
|
|
|
865
934
|
refreshFooter();
|
|
866
935
|
g.__piwiAnchorCleanup = cleanup;
|
|
867
936
|
doc.addEventListener("keydown", onKey, true);
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
937
|
+
const mountParent = () => {
|
|
938
|
+
let modal = null;
|
|
939
|
+
try {
|
|
940
|
+
modal = doc.querySelector("dialog:modal");
|
|
941
|
+
} catch {
|
|
942
|
+
modal = null;
|
|
943
|
+
}
|
|
944
|
+
if (!modal) return doc.body;
|
|
945
|
+
const s = g.getComputedStyle(modal);
|
|
946
|
+
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);
|
|
947
|
+
return holdsFixed ? doc.body : modal;
|
|
948
|
+
};
|
|
949
|
+
const mount = (...nodes) => {
|
|
950
|
+
const parent = mountParent();
|
|
951
|
+
for (const node2 of nodes) parent.appendChild(node2);
|
|
952
|
+
if (parent !== doc.body) {
|
|
953
|
+
parent.addEventListener(
|
|
954
|
+
"close",
|
|
955
|
+
() => {
|
|
956
|
+
for (const node2 of nodes) if (node2.parentNode === parent) doc.body.appendChild(node2);
|
|
957
|
+
},
|
|
958
|
+
{ once: true }
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
mount(pickedOutline, outline, outlineLabel, panel);
|
|
872
963
|
}
|
|
873
964
|
|
|
874
965
|
// ../picker-dom/src/overlay-confirm.ts
|
|
@@ -1068,7 +1159,7 @@ function isAutoGenerated(value) {
|
|
|
1068
1159
|
return false;
|
|
1069
1160
|
}
|
|
1070
1161
|
var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
|
|
1071
|
-
function generateAlternatives(attrs) {
|
|
1162
|
+
function generateAlternatives(attrs, options = {}) {
|
|
1072
1163
|
const alts = [];
|
|
1073
1164
|
const seen = /* @__PURE__ */ new Set();
|
|
1074
1165
|
const add = (loc) => {
|
|
@@ -1079,12 +1170,14 @@ function generateAlternatives(attrs) {
|
|
|
1079
1170
|
};
|
|
1080
1171
|
const { accessibleName } = attrs;
|
|
1081
1172
|
const tag = attrs.tagName;
|
|
1082
|
-
const role = resolveAriaRole(attrs);
|
|
1173
|
+
const role = options.role !== void 0 ? options.role : resolveAriaRole(attrs);
|
|
1083
1174
|
const counts = attrs.selectorCounts;
|
|
1084
1175
|
const isUnique = (n) => n == null || n <= 1;
|
|
1085
1176
|
const ambiguityPenalty = (n) => n != null && n > 1 ? 45 : 0;
|
|
1086
1177
|
const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
|
|
1087
|
-
const
|
|
1178
|
+
const testIdAttribute = options.testIdAttribute || "data-testid";
|
|
1179
|
+
const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
|
|
1180
|
+
const testId = attr(attrs, testIdAttribute);
|
|
1088
1181
|
if (testId && isUnique(counts?.testId)) {
|
|
1089
1182
|
add({
|
|
1090
1183
|
locator: `getByTestId('${esc(testId)}')`,
|
|
@@ -1143,7 +1236,7 @@ function generateAlternatives(attrs) {
|
|
|
1143
1236
|
});
|
|
1144
1237
|
}
|
|
1145
1238
|
const id = attr(attrs, "id");
|
|
1146
|
-
if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
1239
|
+
if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
1147
1240
|
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
1148
1241
|
add({
|
|
1149
1242
|
locator: `locator('${esc(selector)}')`,
|
|
@@ -1153,7 +1246,7 @@ function generateAlternatives(attrs) {
|
|
|
1153
1246
|
});
|
|
1154
1247
|
}
|
|
1155
1248
|
const name = attr(attrs, "name");
|
|
1156
|
-
if (name && isUnique(counts?.name)) {
|
|
1249
|
+
if (name && selectable("name") && isUnique(counts?.name)) {
|
|
1157
1250
|
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
1158
1251
|
add({
|
|
1159
1252
|
locator: `locator('${esc(selector)}')`,
|
|
@@ -1198,7 +1291,7 @@ function generateAlternatives(attrs) {
|
|
|
1198
1291
|
score: scores.testId
|
|
1199
1292
|
});
|
|
1200
1293
|
}
|
|
1201
|
-
if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
1294
|
+
if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
1202
1295
|
idAnchorDone = true;
|
|
1203
1296
|
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
1204
1297
|
add({
|
|
@@ -1208,7 +1301,7 @@ function generateAlternatives(attrs) {
|
|
|
1208
1301
|
score: scores.id
|
|
1209
1302
|
});
|
|
1210
1303
|
}
|
|
1211
|
-
if (!dataAnchorDone && anc.dataAttr && anc.dataAttrCount === 1) {
|
|
1304
|
+
if (!dataAnchorDone && anc.dataAttr && selectable(anc.dataAttr.name) && anc.dataAttrCount === 1) {
|
|
1212
1305
|
dataAnchorDone = true;
|
|
1213
1306
|
const { name: name2, value } = anc.dataAttr;
|
|
1214
1307
|
const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/reporter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Playwright reporter that streams results, traces and HTML reports to a Piwi Dashboard instance",
|
|
5
5
|
"url": "https://github.com/PiwiTests/platform",
|
|
6
6
|
"homepage": "https://piwitests.dev",
|
|
@@ -25,6 +25,8 @@ Prefer the **Piwi MCP server** if it is connected (`get_locator_healing`, `expla
|
|
|
25
25
|
|
|
26
26
|
6. **Report.** Show the before/after locator, the file and line, and the verifying result. If several tests shared the selector, list every place you changed.
|
|
27
27
|
|
|
28
|
+
7. **Report back to Piwi.** Commit with the trailer `Piwi-Cluster: <clusterId>` as the last line of the message (the cluster of the failing case; `get_fix_plan` returns the line in `verify.commitTrailer`), then call `report_fix_attempt` with that cluster, `kind: "locator-edit"`, the commit or the branch, and the `edit` (`filePath`, `line`, `from`: the old locator, `to`: the one you applied). Do it once per call site you changed. Piwi records the attempt verified when the test passes on that commit, and counts the healing suggestion as adopted.
|
|
29
|
+
|
|
28
30
|
## Guardrails
|
|
29
31
|
|
|
30
32
|
- Change the **locator**, not the assertion's intent — if a test checked for "Sign in" and the button is now "Log in", that is a copy change to confirm with the user, not a locator to heal.
|
|
@@ -27,7 +27,11 @@ Prefer the **Piwi MCP server** if it is connected to this agent (tools named `li
|
|
|
27
27
|
|
|
28
28
|
7. **Verify.** Re-run the affected spec(s): `npx playwright test <file>`. Confirm they pass and the new run is green in the dashboard (`get_run_insights` compares against the last green run: regressions cleared, nothing new broken).
|
|
29
29
|
|
|
30
|
-
8. **
|
|
30
|
+
8. **Report back.** Piwi learns from what you tell it, so end every investigation with these calls (they need reporter or admin access; each is logged with your API key):
|
|
31
|
+
- **Your diagnosis.** If the cluster had no diagnosis, or yours differs, record it with `record_diagnosis`: the model you run on and the diagnosis in the schema the tool describes (summary, confidenceScore, severity, ranked hypotheses with their evidence, suggestedFix with the unified-diff `patch`). Piwi validates the patch against the source that failed and shows the diagnosis as written by an agent.
|
|
32
|
+
- **Your rating of Piwi's diagnosis.** If you read one with `get_cluster_diagnosis`, rate it with `submit_diagnosis_feedback`: `up` when it pointed at the real cause, `down` with a one-line note on what it got wrong when it did not. A `down` keeps the next diagnosis from repeating it.
|
|
33
|
+
- **Your fix.** Commit the change with the trailer `get_fix_plan` returns in `verify.commitTrailer` (`Piwi-Cluster: <clusterId>`) as the last line of the commit message, then call `report_fix_attempt` with the cluster, the kind (`patch`, `locator-edit` or `fix-plan`), the commit or the branch, the patch you applied and the `diagnosisId` you followed. When the tests pass on that commit, Piwi records the attempt verified and shows it on the cluster's activity.
|
|
34
|
+
- Leave the cluster's status alone: the run that passes on your commit records the fix.
|
|
31
35
|
|
|
32
36
|
## Guardrails
|
|
33
37
|
|
|
@@ -14,7 +14,7 @@ drafts it from the graph, and opens it in the same change.
|
|
|
14
14
|
## How you reach Piwi
|
|
15
15
|
|
|
16
16
|
Prefer the **Piwi MCP server** if it is connected (`list_scenario_gaps`,
|
|
17
|
-
`draft_scenario`, `get_change_coverage`). Otherwise use the dashboard's **Gaps**
|
|
17
|
+
`draft_scenario`, `get_change_coverage`, `triage_gap`). Otherwise use the dashboard's **Gaps**
|
|
18
18
|
view, or the reporter CLI with `PIWI_DASHBOARD_URL` / `PIWI_API_KEY` /
|
|
19
19
|
`PIWI_PROJECT_NAME` set.
|
|
20
20
|
|
|
@@ -55,11 +55,18 @@ view, or the reporter CLI with `PIWI_DASHBOARD_URL` / `PIWI_API_KEY` /
|
|
|
55
55
|
test asserts the missing behavior. If the gap had no reachable path, say so —
|
|
56
56
|
the draft cannot invent an entry point, and the gap may need a fixture.
|
|
57
57
|
|
|
58
|
+
7. **Report back to Piwi.** Tell Piwi what you decided about the gap with
|
|
59
|
+
`triage_gap` (reporter or admin access): `verb: "accept"` for the gap you
|
|
60
|
+
wrote the test for, `covered-by` with the covering test's id when a test you
|
|
61
|
+
found already covers it, or `dismiss` with the reason (`not-worth-testing`,
|
|
62
|
+
`covered-elsewhere`, `wrong`) when the evidence did not hold. Each verdict counts toward the precision of the
|
|
63
|
+
detector that raised the gap, so a detector that keeps being wrong is muted.
|
|
64
|
+
|
|
58
65
|
## Guardrails
|
|
59
66
|
|
|
60
67
|
- A gap is a suggestion with evidence, never a verdict. If the evidence does not
|
|
61
68
|
hold — the route is headless by design, the control is decorative — dismiss it
|
|
62
|
-
|
|
69
|
+
with `triage_gap` and a reason rather than writing a test that asserts nothing.
|
|
63
70
|
- Do not weaken an assertion to make the draft pass. A test that passes without
|
|
64
71
|
asserting the behavior is the false-comfort gap you were sent to close.
|
|
65
72
|
- The draft's assertion is a `TODO` on purpose: fill it from the source and the
|