@piwitests/reporter 0.58.0 → 0.60.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 +1456 -1305
- package/dist/global-setup-module.js +137 -54
- package/dist/index.d.ts +16 -0
- package/dist/index.js +1321 -494
- package/dist/internal/capture/attachments.d.ts +1 -0
- package/dist/internal/capture/attachments.js +2 -1
- package/dist/internal/capture/capture-fixtures.js +1011 -333
- package/dist/internal/capture/pick-on-failure.d.ts +6 -3
- package/dist/internal/capture/pick-on-failure.js +123 -45
- package/package.json +5 -5
- package/templates/skills/run-the-right-tests/SKILL.md +19 -8
- package/templates/skills/setup-piwi/SKILL.md +4 -1
|
@@ -57,15 +57,18 @@ declare function parseLeafLocatorExpression(rawExpr: string): {
|
|
|
57
57
|
declare function deriveFailedLocator(testInfo: TestInfo): FailedLocatorInfo | null;
|
|
58
58
|
/** What the element → anchors → confirm flow yields: the confirmed locator, its alternatives and the element. */
|
|
59
59
|
type PickedLocator = Omit<UserPickResult, 'failing'>;
|
|
60
|
+
/** `run`, or undefined as soon as `ended` aborts; `run` itself is left to settle on its own. */
|
|
61
|
+
declare function unlessEnded<T>(run: Promise<T>, ended: AbortSignal | undefined): Promise<T | undefined>;
|
|
60
62
|
/**
|
|
61
63
|
* Run the element → anchors → confirm flow on a live page: element pick (snap
|
|
62
64
|
* + tree-walk), optional stable-parent anchoring with live match counts, ranked
|
|
63
65
|
* candidates (standard generation merged with anchor-scoped chains), and a
|
|
64
66
|
* final confirmation. `failing` is the locator the pick replaces, rendered, for
|
|
65
67
|
* the overlay's headings; null to pick any element. Returns null when the
|
|
66
|
-
* human skips
|
|
68
|
+
* human skips, no locator could be generated or `ended` aborts, the overlay
|
|
69
|
+
* then taken out of the page; throws when the page breaks.
|
|
67
70
|
*/
|
|
68
|
-
declare function pickLocator(page: Page, probe: PickerProbe, failing: string | null): Promise<PickedLocator | null>;
|
|
71
|
+
declare function pickLocator(page: Page, probe: PickerProbe, failing: string | null, ended?: AbortSignal): Promise<PickedLocator | null>;
|
|
69
72
|
/**
|
|
70
73
|
* Drive the full pick flow on the failing page (`pickLocator`), the test
|
|
71
74
|
* timeout lifted while waiting. Returns null when skipped or when anything
|
|
@@ -88,4 +91,4 @@ declare function runLocatorPicker(page: Page, testInfo: TestInfo, failed: Failed
|
|
|
88
91
|
*/
|
|
89
92
|
declare function applyPickToSnapshots(snapshots: LocatorSnapshot[], pick: UserPickResult): boolean;
|
|
90
93
|
|
|
91
|
-
export { type PickedLocator, type PickerProbe, type UserPickResult, applyPickToSnapshots, deriveFailedLocator, parseLeafLocatorExpression, pickLocator, runLocatorPicker };
|
|
94
|
+
export { type PickedLocator, type PickerProbe, type UserPickResult, applyPickToSnapshots, deriveFailedLocator, parseLeafLocatorExpression, pickLocator, runLocatorPicker, unlessEnded };
|
|
@@ -39,7 +39,8 @@ __export(pick_on_failure_exports, {
|
|
|
39
39
|
pickLocator: () => pickLocator,
|
|
40
40
|
runLocatorPicker: () => runLocatorPicker,
|
|
41
41
|
showAnchorPicker: () => showAnchorPicker,
|
|
42
|
-
showPickerChoices: () => showPickerChoices
|
|
42
|
+
showPickerChoices: () => showPickerChoices,
|
|
43
|
+
unlessEnded: () => unlessEnded
|
|
43
44
|
});
|
|
44
45
|
module.exports = __toCommonJS(pick_on_failure_exports);
|
|
45
46
|
var path2 = __toESM(require("path"));
|
|
@@ -379,10 +380,11 @@ function installPickerOverlay(arg) {
|
|
|
379
380
|
}
|
|
380
381
|
const Z = 2147483600;
|
|
381
382
|
const testIdAttr = arg.testIdAttribute || "data-testid";
|
|
383
|
+
const enterReaches = arg.transport === "global";
|
|
382
384
|
const words = {
|
|
383
385
|
banner: "Piwi inspector \u2014 click any element to generate locators for it",
|
|
384
|
-
keys: "\u2191 parent \xB7 \u2193 child \xB7 Esc skip",
|
|
385
|
-
keysHovering: "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip",
|
|
386
|
+
keys: enterReaches ? "Enter picks the focused element \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip" : "\u2191 parent \xB7 \u2193 child \xB7 Esc skip",
|
|
387
|
+
keysHovering: enterReaches ? "click or Enter to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip" : "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip",
|
|
386
388
|
analyzing: "Analyzing element\u2026"
|
|
387
389
|
};
|
|
388
390
|
for (const key of Object.keys(words)) {
|
|
@@ -477,6 +479,16 @@ function installPickerOverlay(arg) {
|
|
|
477
479
|
const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
|
|
478
480
|
return cls ? `locator(${quoteJs(`.${cls}`)})` : tag;
|
|
479
481
|
};
|
|
482
|
+
const parentOf = (node) => {
|
|
483
|
+
if (node.parentElement) return node.parentElement;
|
|
484
|
+
const root = node.getRootNode ? node.getRootNode() : null;
|
|
485
|
+
return root && root !== doc && root.host ? root.host : null;
|
|
486
|
+
};
|
|
487
|
+
const targetOf = (e) => {
|
|
488
|
+
const path3 = typeof e.composedPath === "function" ? e.composedPath() : null;
|
|
489
|
+
const first = path3 && path3.length ? path3[0] : null;
|
|
490
|
+
return first && first.nodeType === 1 ? first : e.target;
|
|
491
|
+
};
|
|
480
492
|
const buildChain = (raw) => {
|
|
481
493
|
const chain2 = [];
|
|
482
494
|
let node = raw;
|
|
@@ -484,7 +496,7 @@ function installPickerOverlay(arg) {
|
|
|
484
496
|
const tag = (node.tagName || "").toLowerCase();
|
|
485
497
|
if (tag === "body" || tag === "html") break;
|
|
486
498
|
chain2.push(node);
|
|
487
|
-
node = node
|
|
499
|
+
node = parentOf(node);
|
|
488
500
|
}
|
|
489
501
|
return chain2.length ? chain2 : [raw];
|
|
490
502
|
};
|
|
@@ -575,10 +587,14 @@ function installPickerOverlay(arg) {
|
|
|
575
587
|
}
|
|
576
588
|
};
|
|
577
589
|
const onMove = (e) => {
|
|
578
|
-
const raw = e
|
|
590
|
+
const raw = targetOf(e);
|
|
579
591
|
if (!raw || isOwn(raw)) {
|
|
592
|
+
chain = [];
|
|
593
|
+
idx = 0;
|
|
594
|
+
lastRaw = null;
|
|
580
595
|
highlight.style.display = "none";
|
|
581
596
|
label.style.display = "none";
|
|
597
|
+
foot.textContent = words.keys;
|
|
582
598
|
return;
|
|
583
599
|
}
|
|
584
600
|
if (raw !== lastRaw) {
|
|
@@ -657,17 +673,30 @@ function installPickerOverlay(arg) {
|
|
|
657
673
|
const onClick = (e) => {
|
|
658
674
|
stop(e);
|
|
659
675
|
const el = current();
|
|
660
|
-
if (!el || isOwn(e
|
|
676
|
+
if (!el || isOwn(targetOf(e))) return;
|
|
661
677
|
reportPicked(el);
|
|
662
678
|
};
|
|
679
|
+
const focused = () => {
|
|
680
|
+
let el = doc.activeElement;
|
|
681
|
+
while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement;
|
|
682
|
+
const tag = (el && el.tagName || "").toLowerCase();
|
|
683
|
+
return !el || tag === "body" || tag === "html" || isOwn(el) ? null : el;
|
|
684
|
+
};
|
|
663
685
|
const onKey = (e) => {
|
|
664
686
|
if (e.key === "Escape") {
|
|
665
687
|
stop(e);
|
|
666
688
|
reportSkipped();
|
|
667
689
|
return;
|
|
668
690
|
}
|
|
691
|
+
if (e.key === "Enter" && !e.isComposing) {
|
|
692
|
+
stop(e);
|
|
693
|
+
const el = current() || focused();
|
|
694
|
+
if (el) reportPicked(el);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
669
697
|
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
670
698
|
stop(e);
|
|
699
|
+
if (!chain.length) return;
|
|
671
700
|
if (e.key === "ArrowUp") idx = Math.min(idx + 1, chain.length - 1);
|
|
672
701
|
else idx = Math.max(idx - 1, 0);
|
|
673
702
|
refresh();
|
|
@@ -741,12 +770,43 @@ function showAnchorPicker(arg) {
|
|
|
741
770
|
const al = n.getAttribute && n.getAttribute("aria-level");
|
|
742
771
|
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
743
772
|
};
|
|
773
|
+
const parentOf = (n) => {
|
|
774
|
+
if (n.parentElement) return n.parentElement;
|
|
775
|
+
const root = n.getRootNode ? n.getRootNode() : null;
|
|
776
|
+
return root && root !== doc && root.host ? root.host : null;
|
|
777
|
+
};
|
|
778
|
+
const shadowRoots = [];
|
|
779
|
+
const collectRoots = (root) => {
|
|
780
|
+
const all = root.querySelectorAll("*");
|
|
781
|
+
for (let i = 0; i < all.length; i++) {
|
|
782
|
+
const sr = all[i].shadowRoot;
|
|
783
|
+
if (sr) {
|
|
784
|
+
shadowRoots.push(sr);
|
|
785
|
+
collectRoots(sr);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
try {
|
|
790
|
+
collectRoots(doc);
|
|
791
|
+
} catch {
|
|
792
|
+
shadowRoots.length = 0;
|
|
793
|
+
}
|
|
794
|
+
const within = (node2, scope) => {
|
|
795
|
+
if (scope === doc) return true;
|
|
796
|
+
for (let n = node2; n; n = parentOf(n)) if (n === scope) return true;
|
|
797
|
+
return false;
|
|
798
|
+
};
|
|
799
|
+
const queryAll = (scope, sel) => {
|
|
800
|
+
const found = Array.from(scope.querySelectorAll(sel));
|
|
801
|
+
for (const sr of shadowRoots) if (within(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
|
|
802
|
+
return found;
|
|
803
|
+
};
|
|
744
804
|
const leafCountable = !!leafTestId || roleOf(el) === leafRole;
|
|
745
805
|
const leafMatches = (scope) => {
|
|
746
806
|
try {
|
|
747
|
-
if (leafTestId) return scope
|
|
807
|
+
if (leafTestId) return queryAll(scope, `[${testIdAttr}=${JSON.stringify(leafTestId)}]`).length;
|
|
748
808
|
if (!leafCountable) return -1;
|
|
749
|
-
const nodes = scope
|
|
809
|
+
const nodes = queryAll(scope, roleSources);
|
|
750
810
|
if (nodes.length > 2e3) return -1;
|
|
751
811
|
let matched = 0;
|
|
752
812
|
for (let i = 0; i < nodes.length; i++) {
|
|
@@ -762,20 +822,20 @@ function showAnchorPicker(arg) {
|
|
|
762
822
|
};
|
|
763
823
|
let roleNodes = [];
|
|
764
824
|
try {
|
|
765
|
-
const all = doc
|
|
766
|
-
if (all.length <= 4e3) roleNodes =
|
|
825
|
+
const all = queryAll(doc, roleSources);
|
|
826
|
+
if (all.length <= 4e3) roleNodes = all;
|
|
767
827
|
} catch {
|
|
768
828
|
roleNodes = [];
|
|
769
829
|
}
|
|
770
830
|
const count = (sel) => {
|
|
771
831
|
try {
|
|
772
|
-
return doc
|
|
832
|
+
return queryAll(doc, sel).length;
|
|
773
833
|
} catch {
|
|
774
834
|
return void 0;
|
|
775
835
|
}
|
|
776
836
|
};
|
|
777
837
|
const rows = [];
|
|
778
|
-
let node = el
|
|
838
|
+
let node = parentOf(el);
|
|
779
839
|
let depth = 0;
|
|
780
840
|
while (node && depth < 12) {
|
|
781
841
|
depth++;
|
|
@@ -812,7 +872,7 @@ function showAnchorPicker(arg) {
|
|
|
812
872
|
hookLabel,
|
|
813
873
|
selectable: !!(testId || id || role && (ariaLabel || info.roleCount === 1))
|
|
814
874
|
});
|
|
815
|
-
node = node
|
|
875
|
+
node = parentOf(node);
|
|
816
876
|
}
|
|
817
877
|
if (rows.length === 0) {
|
|
818
878
|
g.__piwiAnchorState = "skipped";
|
|
@@ -841,9 +901,9 @@ function showAnchorPicker(arg) {
|
|
|
841
901
|
footer.style.cssText = "margin:10px 0;font-weight:600;";
|
|
842
902
|
const segMatches = (scope, info) => {
|
|
843
903
|
try {
|
|
844
|
-
if (info.testId) return
|
|
845
|
-
if (info.id) return
|
|
846
|
-
const nodes =
|
|
904
|
+
if (info.testId) return queryAll(scope, `[${testIdAttr}=${JSON.stringify(info.testId)}]`);
|
|
905
|
+
if (info.id) return queryAll(scope, `#${doc.defaultView.CSS.escape(info.id)}`);
|
|
906
|
+
const nodes = queryAll(scope, roleSources);
|
|
847
907
|
if (nodes.length > 2e3) return [];
|
|
848
908
|
return nodes.filter(
|
|
849
909
|
(n) => roleOf(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
|
|
@@ -1557,32 +1617,55 @@ ${err.stack ?? ""}`));
|
|
|
1557
1617
|
}
|
|
1558
1618
|
return null;
|
|
1559
1619
|
}
|
|
1560
|
-
|
|
1620
|
+
var PICK_ENDED = "__piwiPickEnded";
|
|
1621
|
+
async function cleanupPicker(page, ended = false) {
|
|
1561
1622
|
try {
|
|
1562
|
-
await page.evaluate(
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1623
|
+
await page.evaluate(
|
|
1624
|
+
([endedKey, unanswered]) => {
|
|
1625
|
+
const g = globalThis;
|
|
1626
|
+
if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
|
|
1627
|
+
if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
|
|
1628
|
+
delete g.__piwiPickCleanup;
|
|
1629
|
+
delete g.__piwiAnchorCleanup;
|
|
1630
|
+
delete g.__piwiPickState;
|
|
1631
|
+
delete g.__piwiPickChoice;
|
|
1632
|
+
delete g.__piwiPickedElement;
|
|
1633
|
+
delete g.__piwiAnchorState;
|
|
1634
|
+
delete g.__piwiPickAnchors;
|
|
1635
|
+
delete g.__piwiPickChainCount;
|
|
1636
|
+
if (unanswered) g[endedKey] = true;
|
|
1637
|
+
else delete g[endedKey];
|
|
1638
|
+
},
|
|
1639
|
+
[PICK_ENDED, ended]
|
|
1640
|
+
);
|
|
1575
1641
|
} catch {
|
|
1576
1642
|
}
|
|
1577
1643
|
}
|
|
1578
|
-
|
|
1644
|
+
function unlessEnded(run, ended) {
|
|
1645
|
+
if (!ended) return run;
|
|
1646
|
+
return Promise.race([
|
|
1647
|
+
run,
|
|
1648
|
+
new Promise((resolve) => {
|
|
1649
|
+
if (ended.aborted) resolve(void 0);
|
|
1650
|
+
else ended.addEventListener("abort", () => resolve(void 0), { once: true });
|
|
1651
|
+
})
|
|
1652
|
+
]);
|
|
1653
|
+
}
|
|
1654
|
+
async function pickLocator(page, probe, failing, ended) {
|
|
1655
|
+
const answered = async (name) => {
|
|
1656
|
+
const wait = page.waitForFunction(
|
|
1657
|
+
([key, endedKey]) => globalThis[key] !== void 0 || globalThis[endedKey] === true,
|
|
1658
|
+
[name, PICK_ENDED],
|
|
1659
|
+
{ timeout: 0, polling: 250 }
|
|
1660
|
+
);
|
|
1661
|
+
await unlessEnded(wait, ended);
|
|
1662
|
+
return !ended?.aborted;
|
|
1663
|
+
};
|
|
1579
1664
|
try {
|
|
1580
1665
|
const overlayArg = { transport: "global", failing };
|
|
1666
|
+
await page.evaluate((endedKey) => delete globalThis[endedKey], PICK_ENDED);
|
|
1581
1667
|
await page.evaluate(installPickerOverlay, overlayArg);
|
|
1582
|
-
await
|
|
1583
|
-
timeout: 0,
|
|
1584
|
-
polling: 250
|
|
1585
|
-
});
|
|
1668
|
+
if (!await answered("__piwiPickState")) return null;
|
|
1586
1669
|
const state = await page.evaluate(() => globalThis.__piwiPickState);
|
|
1587
1670
|
if (state !== "picked") return null;
|
|
1588
1671
|
const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
|
|
@@ -1603,10 +1686,7 @@ async function pickLocator(page, probe, failing) {
|
|
|
1603
1686
|
leafLevel: level,
|
|
1604
1687
|
leafTestId: attrs.attributes["data-testid"] ?? null
|
|
1605
1688
|
});
|
|
1606
|
-
await
|
|
1607
|
-
timeout: 0,
|
|
1608
|
-
polling: 250
|
|
1609
|
-
});
|
|
1689
|
+
if (!await answered("__piwiAnchorState")) return null;
|
|
1610
1690
|
const anchorResult = await page.evaluate(() => {
|
|
1611
1691
|
const g = globalThis;
|
|
1612
1692
|
return { state: g.__piwiAnchorState, anchors: g.__piwiPickAnchors ?? [], chainCount: g.__piwiPickChainCount };
|
|
@@ -1628,10 +1708,7 @@ async function pickLocator(page, probe, failing) {
|
|
|
1628
1708
|
failing,
|
|
1629
1709
|
choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
|
|
1630
1710
|
});
|
|
1631
|
-
await
|
|
1632
|
-
timeout: 0,
|
|
1633
|
-
polling: 250
|
|
1634
|
-
});
|
|
1711
|
+
if (!await answered("__piwiPickChoice")) return null;
|
|
1635
1712
|
const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
|
|
1636
1713
|
if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
|
|
1637
1714
|
const picked = { ...ranked[choice], pickedByUser: true };
|
|
@@ -1650,7 +1727,7 @@ async function pickLocator(page, probe, failing) {
|
|
|
1650
1727
|
...anchors.length > 0 ? { anchors } : {}
|
|
1651
1728
|
};
|
|
1652
1729
|
} finally {
|
|
1653
|
-
await cleanupPicker(page);
|
|
1730
|
+
await cleanupPicker(page, ended?.aborted);
|
|
1654
1731
|
}
|
|
1655
1732
|
}
|
|
1656
1733
|
async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
@@ -1714,5 +1791,6 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
1714
1791
|
pickLocator,
|
|
1715
1792
|
runLocatorPicker,
|
|
1716
1793
|
showAnchorPicker,
|
|
1717
|
-
showPickerChoices
|
|
1794
|
+
showPickerChoices,
|
|
1795
|
+
unlessEnded
|
|
1718
1796
|
});
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/reporter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"description": "Playwright reporter that streams results, traces and HTML reports to a Piwi Dashboard instance",
|
|
5
|
-
"url": "https://github.com/PiwiTests/
|
|
5
|
+
"url": "https://github.com/PiwiTests/piwi",
|
|
6
6
|
"homepage": "https://piwitests.dev",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/PiwiTests/
|
|
9
|
+
"url": "https://github.com/PiwiTests/piwi"
|
|
10
10
|
},
|
|
11
11
|
"bugs": {
|
|
12
|
-
"url": "https://github.com/PiwiTests/
|
|
12
|
+
"url": "https://github.com/PiwiTests/piwi/issues"
|
|
13
13
|
},
|
|
14
14
|
"type": "commonjs",
|
|
15
15
|
"main": "dist/index.js",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"node": ">=20"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@playwright/test": "^1.
|
|
69
|
+
"@playwright/test": "^1.64.1"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"form-data": "^4.0.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: run-the-right-tests
|
|
3
|
-
description: Pick and run the right subset of Playwright tests using Piwi's data-driven selections, instead of always running the whole suite. Use when the user asks to "run smoke tests", "run the right tests", "just run what's relevant", "verify this fix", or wants a fast, targeted test loop rather than the full run.
|
|
3
|
+
description: Pick and run the right subset of Playwright tests using Piwi's data-driven selections and the tests a change impacts, instead of always running the whole suite. Use when the user asks to "run smoke tests", "run the right tests", "just run what's relevant", "run the tests my change affects", "verify this fix", or wants a fast, targeted test loop rather than the full run.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Run the right tests with Piwi selections
|
|
@@ -9,8 +9,8 @@ Running the whole suite for every change is slow, and hand-maintained grep patte
|
|
|
9
9
|
|
|
10
10
|
## How you reach Piwi
|
|
11
11
|
|
|
12
|
-
Prefer the **Piwi MCP server** if it is connected (`
|
|
13
|
-
`predict_locator_breaks`). Otherwise use the reporter CLI (`npx @piwitests/reporter select|run|preflight`) with `PIWI_DASHBOARD_URL` / `PIWI_API_KEY` / `PIWI_PROJECT_NAME` set, or the dashboard's **Selections** tab.
|
|
12
|
+
Prefer the **Piwi MCP server** if it is connected (`get_impacted_tests`, `list_selections`, `resolve_selection`,
|
|
13
|
+
`preview_selection`, `predict_locator_breaks`). Otherwise use the reporter CLI (`npx @piwitests/reporter select|run|preflight`) with `PIWI_DASHBOARD_URL` / `PIWI_API_KEY` / `PIWI_PROJECT_NAME` set, or the dashboard's **Selections** tab.
|
|
14
14
|
|
|
15
15
|
## Steps
|
|
16
16
|
|
|
@@ -25,22 +25,33 @@ Prefer the **Piwi MCP server** if it is connected (`list_selections`, `resolve_s
|
|
|
25
25
|
your `git diff` and returns the same breaks and edits. A prediction is not a failure: say which breaks you fixed and
|
|
26
26
|
which you left, and why.
|
|
27
27
|
|
|
28
|
-
1. **
|
|
28
|
+
1. **After any code change, run the tests it impacts.** Over MCP, call `get_impacted_tests` with your diff
|
|
29
|
+
(`git diff --full-index <base>`, the base being the commit your change starts from, such as `origin/main`). Each
|
|
30
|
+
changed file lists the tests it selects and how: `lines` (the test ran a changed line), `functions` (it ran the
|
|
31
|
+
function around one), `file` (its coverage cannot rule it out), `test-file`, `frames` or `route`. Run the
|
|
32
|
+
`command` it returns, or, from the CLI, `npx @piwitests/reporter run impact --base origin/main`. Read the answer
|
|
33
|
+
before trusting it:
|
|
34
|
+
- `widened` means a changed file maps to no test, so the command runs the whole suite. Say which files did that.
|
|
35
|
+
- A file's `notRun` lines are changed code no test runs: no test can catch a mistake there. Name them, and offer
|
|
36
|
+
to write the test (the `write-the-missing-test` skill).
|
|
37
|
+
- The tests marked `lines` or `test-file` are the first to check for edits a change in behavior needs.
|
|
38
|
+
|
|
39
|
+
2. **See what selections exist.** Call `list_selections` for the project. Every project has the built-ins `failed` (tests whose latest run failed) and `quarantine-free`, plus any the team saved (often `smoke`). Pick the one that matches the intent:
|
|
29
40
|
- About to make a broad change, or want a quick confidence check → `smoke` (or `preview_selection` with `{ "include": [{ "tags": ["smoke"] }] }`).
|
|
30
41
|
- Iterating on a fix → `failed`, or a `{ "include": [{ "failedInLastRuns": 5 }] }` preview.
|
|
31
42
|
- Time-boxed → resolve any selection with a `budgetMs` (e.g. 300000 for five minutes).
|
|
32
43
|
|
|
33
|
-
|
|
44
|
+
3. **Resolve it to a command.** Call `resolve_selection` with the key (and optional `budgetMs`). You get back the matching tests, an estimate, and a ready-to-run `playwright test` command. Check the count and warnings first — a selection that matches **zero** tests is a red flag (too narrow, or nothing qualifies), and a `quarantined-included` warning means a flaky test is in the set.
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
4. **Run it.** The most reliable way is the reporter CLI, which resolves and runs in one step and stamps the run so the dashboard names the subset:
|
|
36
47
|
```
|
|
37
48
|
npx @piwitests/reporter run <key>
|
|
38
49
|
```
|
|
39
50
|
Or run the materialized command `resolve_selection` returned directly with `playwright test`. Pass extra Playwright args after `--` (e.g. `-- --workers=4`).
|
|
40
51
|
|
|
41
|
-
|
|
52
|
+
5. **After a fix, verify with the same selection.** Re-run the selection that covered the failure and confirm it now passes. In CI, `npx @piwitests/reporter gate --require-selection <key>` fails the build if any test the selection currently matches did not run or did not pass — catching a smoke job that silently shrank.
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
6. **Report.** State which selection you ran, how many tests it resolved to, and the outcome. If no saved selection fit, suggest one (a `preview_selection` definition the team could save) rather than falling back to the whole suite silently.
|
|
44
55
|
|
|
45
56
|
## Guardrails
|
|
46
57
|
|
|
@@ -13,6 +13,9 @@ Find out two things (ask the user only if you cannot infer them):
|
|
|
13
13
|
|
|
14
14
|
- **Dashboard URL** — e.g. `http://localhost:3000` for a local server, or a deployed URL. Defaults to `http://localhost:3000`.
|
|
15
15
|
- **Project name** — the label runs are grouped under. Defaults to the package or folder name.
|
|
16
|
+
- **Where local runs go** — when developers run the Piwi desktop app and CI reports to a shared server, add
|
|
17
|
+
`--desktop-app prefer`: a local run lands in the app while it is running, and CI runs go to `--server-url`. When the
|
|
18
|
+
desktop app is the only dashboard, pass `--no-server-url` instead of a URL: the reporter finds the running app itself.
|
|
16
19
|
|
|
17
20
|
Confirm this is a Playwright project: there should be a `playwright.config.ts|js` and `@playwright/test` in `package.json`. If there is no Playwright at all, set that up first.
|
|
18
21
|
|
|
@@ -47,7 +50,7 @@ change it to import from the fixtures file instead, e.g. `import { test, expect
|
|
|
47
50
|
|
|
48
51
|
## Authentication
|
|
49
52
|
|
|
50
|
-
If the dashboard has auth enabled, runs need an API key. Have the user create one in the dashboard (**Settings → Account → API keys**; keys start with `pd_`), then put it in `.env` as `PIWI_API_KEY=pd_...` and make sure `.env` is git-ignored. Never hardcode a key into `playwright.config.ts` or commit it. In CI, pass it as a secret (`PIWI_API_KEY`).
|
|
53
|
+
If the dashboard has auth enabled, runs need an API key. Have the user create one in the dashboard (**Settings → Account → API keys**; keys start with `pd_`), then put it in `.env` as `PIWI_API_KEY=pd_...` and make sure `.env` is git-ignored. The reporter reads `PIWI_DASHBOARD_URL`, `PIWI_API_KEY` and `PIWI_PROJECT_NAME` from the `.env` next to `playwright.config`, so the config needs no dotenv loading; a variable set in the environment wins over the file. Never hardcode a key into `playwright.config.ts` or commit it. In CI, where `.env` is not checked out, pass it as a secret (`PIWI_API_KEY`).
|
|
51
54
|
|
|
52
55
|
## Verify it worked
|
|
53
56
|
|