@piwitests/reporter 0.59.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.
@@ -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 or no locator could be generated; throws when the page breaks.
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"));
@@ -1616,32 +1617,55 @@ ${err.stack ?? ""}`));
1616
1617
  }
1617
1618
  return null;
1618
1619
  }
1619
- async function cleanupPicker(page) {
1620
+ var PICK_ENDED = "__piwiPickEnded";
1621
+ async function cleanupPicker(page, ended = false) {
1620
1622
  try {
1621
- await page.evaluate(() => {
1622
- const g = globalThis;
1623
- if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
1624
- if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
1625
- delete g.__piwiPickCleanup;
1626
- delete g.__piwiAnchorCleanup;
1627
- delete g.__piwiPickState;
1628
- delete g.__piwiPickChoice;
1629
- delete g.__piwiPickedElement;
1630
- delete g.__piwiAnchorState;
1631
- delete g.__piwiPickAnchors;
1632
- delete g.__piwiPickChainCount;
1633
- });
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
+ );
1634
1641
  } catch {
1635
1642
  }
1636
1643
  }
1637
- async function pickLocator(page, probe, failing) {
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
+ };
1638
1664
  try {
1639
1665
  const overlayArg = { transport: "global", failing };
1666
+ await page.evaluate((endedKey) => delete globalThis[endedKey], PICK_ENDED);
1640
1667
  await page.evaluate(installPickerOverlay, overlayArg);
1641
- await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
1642
- timeout: 0,
1643
- polling: 250
1644
- });
1668
+ if (!await answered("__piwiPickState")) return null;
1645
1669
  const state = await page.evaluate(() => globalThis.__piwiPickState);
1646
1670
  if (state !== "picked") return null;
1647
1671
  const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
@@ -1662,10 +1686,7 @@ async function pickLocator(page, probe, failing) {
1662
1686
  leafLevel: level,
1663
1687
  leafTestId: attrs.attributes["data-testid"] ?? null
1664
1688
  });
1665
- await page.waitForFunction(() => globalThis.__piwiAnchorState !== void 0, void 0, {
1666
- timeout: 0,
1667
- polling: 250
1668
- });
1689
+ if (!await answered("__piwiAnchorState")) return null;
1669
1690
  const anchorResult = await page.evaluate(() => {
1670
1691
  const g = globalThis;
1671
1692
  return { state: g.__piwiAnchorState, anchors: g.__piwiPickAnchors ?? [], chainCount: g.__piwiPickChainCount };
@@ -1687,10 +1708,7 @@ async function pickLocator(page, probe, failing) {
1687
1708
  failing,
1688
1709
  choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
1689
1710
  });
1690
- await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
1691
- timeout: 0,
1692
- polling: 250
1693
- });
1711
+ if (!await answered("__piwiPickChoice")) return null;
1694
1712
  const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
1695
1713
  if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
1696
1714
  const picked = { ...ranked[choice], pickedByUser: true };
@@ -1709,7 +1727,7 @@ async function pickLocator(page, probe, failing) {
1709
1727
  ...anchors.length > 0 ? { anchors } : {}
1710
1728
  };
1711
1729
  } finally {
1712
- await cleanupPicker(page);
1730
+ await cleanupPicker(page, ended?.aborted);
1713
1731
  }
1714
1732
  }
1715
1733
  async function runLocatorPicker(page, testInfo, failed, probe) {
@@ -1773,5 +1791,6 @@ function applyPickToSnapshots(snapshots, pick) {
1773
1791
  pickLocator,
1774
1792
  runLocatorPicker,
1775
1793
  showAnchorPicker,
1776
- showPickerChoices
1794
+ showPickerChoices,
1795
+ unlessEnded
1777
1796
  });
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@piwitests/reporter",
3
- "version": "0.59.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/platform",
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/platform"
9
+ "url": "https://github.com/PiwiTests/piwi"
10
10
  },
11
11
  "bugs": {
12
- "url": "https://github.com/PiwiTests/platform/issues"
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.61.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 (`list_selections`, `resolve_selection`, `preview_selection`,
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. **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:
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
- 2. **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.
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
- 3. **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:
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
- 4. **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.
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
- 5. **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.
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