@piwitests/reporter 0.24.0 → 0.26.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.
@@ -309,7 +309,16 @@ function probeElementAttrs(el, arg) {
309
309
  }
310
310
  }
311
311
  const hasLabel = !!(el.labels && el.labels.length > 0);
312
- const labelText = includeLabelText ? hasLabel ? (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null : null : void 0;
312
+ let labelText;
313
+ if (includeLabelText) {
314
+ labelText = null;
315
+ try {
316
+ const label = hasLabel ? el.labels[0] : null;
317
+ labelText = (label && label.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null;
318
+ } catch {
319
+ labelText = null;
320
+ }
321
+ }
313
322
  return {
314
323
  tagName: el.tagName?.toLowerCase?.() ?? "unknown",
315
324
  attributes: attrMap,
@@ -1013,6 +1022,7 @@ var CAPTURED_ATTRIBUTES = [
1013
1022
  "alt",
1014
1023
  "title",
1015
1024
  "aria-label",
1025
+ "aria-labelledby",
1016
1026
  "aria-level",
1017
1027
  "role",
1018
1028
  "type",
@@ -1548,6 +1558,43 @@ function extractAccessibleName(ariaSnapshot) {
1548
1558
  if (match) return match[1];
1549
1559
  return null;
1550
1560
  }
1561
+ var FORM_FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
1562
+ var NAME_FROM_CONTENT_ROLES = /* @__PURE__ */ new Set([
1563
+ "button",
1564
+ "cell",
1565
+ "checkbox",
1566
+ "columnheader",
1567
+ "gridcell",
1568
+ "heading",
1569
+ "link",
1570
+ "menuitem",
1571
+ "menuitemcheckbox",
1572
+ "menuitemradio",
1573
+ "option",
1574
+ "radio",
1575
+ "row",
1576
+ "rowheader",
1577
+ "switch",
1578
+ "tab",
1579
+ "tooltip",
1580
+ "treeitem"
1581
+ ]);
1582
+ var PROBE_TEXT_CAP = 80;
1583
+ var PROBE_LABEL_TEXT_CAP = 120;
1584
+ function exactAccessibleName(attrs, role) {
1585
+ if (attrs.attributes["aria-labelledby"]) return null;
1586
+ const ariaLabel = attrs.attributes["aria-label"];
1587
+ if (ariaLabel) return ariaLabel;
1588
+ if (FORM_FIELD_TAGS.has(attrs.tagName)) {
1589
+ const labelText = attrs.labelText;
1590
+ return labelText && labelText.length < PROBE_LABEL_TEXT_CAP ? labelText : null;
1591
+ }
1592
+ if (role && NAME_FROM_CONTENT_ROLES.has(role)) {
1593
+ const text = attrs.textContent;
1594
+ return text && text.length < PROBE_TEXT_CAP ? text : null;
1595
+ }
1596
+ return null;
1597
+ }
1551
1598
  var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
1552
1599
  "getByText",
1553
1600
  "getByRole",
@@ -2009,7 +2056,7 @@ function createSink() {
2009
2056
  capturedLocators: [],
2010
2057
  capturePromises: [],
2011
2058
  failedLocators: [],
2012
- expectCapturedLocations: /* @__PURE__ */ new Set(),
2059
+ probedLocations: /* @__PURE__ */ new Set(),
2013
2060
  lastActivePage: null,
2014
2061
  testInfo: null,
2015
2062
  stashedWebVitals: null,
@@ -2236,7 +2283,6 @@ var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
2236
2283
  var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
2237
2284
  var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
2238
2285
  var ACTION_METHOD_SET = new Set(ACTION_METHODS);
2239
- var FORM_FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
2240
2286
  var CAPTURED_ATTRS_ARG = {
2241
2287
  keep: [...CAPTURED_ATTRIBUTES],
2242
2288
  tagRoles: TAG_TO_ROLE,
@@ -2245,11 +2291,32 @@ var CAPTURED_ATTRS_ARG = {
2245
2291
  // special-cased logic in the probe, so add them explicitly).
2246
2292
  roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
2247
2293
  // The reporter always wants ancestor-anchored alternatives (the picker's
2248
- // anchors step and generateAnchoredAlternatives both need them); it derives
2249
- // the accessible name itself, so the probe's own labelText is unneeded.
2294
+ // anchors step and generateAnchoredAlternatives both need them). `labelText`
2295
+ // is what names a form field, and the probe reads it from `el.labels` in the
2296
+ // same pass — so asking for it here is free and settles the accessible name
2297
+ // of a labeled field without a second round trip (`exactAccessibleName`).
2250
2298
  includeStructural: true,
2251
- includeLabelText: false
2299
+ includeLabelText: true
2252
2300
  };
2301
+ var PROBE_GLOBAL = "__piwiProbeElement";
2302
+ var PROBE_INIT_SCRIPT = `(() => {
2303
+ const probe = ${probeElementAttrs.toString()};
2304
+ const arg = ${JSON.stringify(CAPTURED_ATTRS_ARG)};
2305
+ globalThis[${JSON.stringify(PROBE_GLOBAL)}] = (el) => probe(el, arg);
2306
+ })();`;
2307
+ var PROBE_UNSEEDED_PAGES = /* @__PURE__ */ new WeakSet();
2308
+ function probeElement(page, target) {
2309
+ const shipSource = () => target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
2310
+ if (!page || PROBE_UNSEEDED_PAGES.has(page)) return shipSource();
2311
+ return target.evaluate((el, name) => {
2312
+ const seeded = globalThis[name];
2313
+ return typeof seeded === "function" ? seeded(el) : null;
2314
+ }, PROBE_GLOBAL).then((attrs) => {
2315
+ if (attrs) return attrs;
2316
+ PROBE_UNSEEDED_PAGES.add(page);
2317
+ return shipSource();
2318
+ });
2319
+ }
2253
2320
  async function ariaSnapshotBestEffort(target, timeout) {
2254
2321
  if (typeof target.ariaSnapshot !== "function") return null;
2255
2322
  try {
@@ -2268,8 +2335,8 @@ async function ariaSnapshotBestEffort(target, timeout) {
2268
2335
  }
2269
2336
  }
2270
2337
  }
2271
- function startElementCapture(sink, target, seq, callerLocation, used) {
2272
- const probe = target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
2338
+ function startElementCapture(sink, page, target, seq, callerLocation, used) {
2339
+ const probe = probeElement(page, target);
2273
2340
  const settledProbe = probe.then(
2274
2341
  () => void 0,
2275
2342
  () => void 0
@@ -2288,8 +2355,9 @@ function startElementCapture(sink, target, seq, callerLocation, used) {
2288
2355
  ]);
2289
2356
  const role = resolveAriaRole({ ...attrs, accessibleName: null });
2290
2357
  const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
2291
- const aria = role || isFormField ? await ariaSnapshotBestEffort(target, 500) : null;
2292
- const accessibleName = extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null });
2358
+ const exactName = exactAccessibleName(attrs, role);
2359
+ const aria = exactName === null && (role || isFormField) ? await ariaSnapshotBestEffort(target, 500) : null;
2360
+ const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
2293
2361
  sink.capturedLocators[seq] = {
2294
2362
  location: callerLocation,
2295
2363
  used,
@@ -2309,6 +2377,7 @@ function startElementCapture(sink, target, seq, callerLocation, used) {
2309
2377
  alternatives: generateAlternatives({ ...attrs, accessibleName })
2310
2378
  };
2311
2379
  } catch {
2380
+ if (callerLocation) sink.probedLocations.delete(callerLocation);
2312
2381
  } finally {
2313
2382
  clearTimeout(deadline);
2314
2383
  }
@@ -2345,17 +2414,17 @@ function wrapLocator(page, locator, originMethod, originArgs) {
2345
2414
  args: originArgs,
2346
2415
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
2347
2416
  };
2348
- const alreadyCaptured = callerLocation !== null && sink.expectCapturedLocations.has(callerLocation);
2417
+ const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
2349
2418
  let seq = -1;
2350
- if (!alreadyCaptured) {
2419
+ if (!alreadyProbed) {
2351
2420
  seq = sink.capturedLocators.length;
2352
2421
  sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
2353
2422
  }
2354
2423
  const result = await fn.apply(target, callArgs);
2355
2424
  const matches = result?.matches;
2356
- if (matches === true && !alreadyCaptured) {
2357
- if (callerLocation) sink.expectCapturedLocations.add(callerLocation);
2358
- startElementCapture(sink, target, seq, callerLocation, used);
2425
+ if (matches === true && !alreadyProbed) {
2426
+ if (callerLocation) sink.probedLocations.add(callerLocation);
2427
+ startElementCapture(sink, page, target, seq, callerLocation, used);
2359
2428
  } else if (matches === false) {
2360
2429
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
2361
2430
  }
@@ -2367,19 +2436,23 @@ function wrapLocator(page, locator, originMethod, originArgs) {
2367
2436
  const sink = currentSink;
2368
2437
  if (!sink) return fn.apply(target, callArgs);
2369
2438
  sink.lastActivePage = page;
2370
- const seq = sink.capturedLocators.length;
2371
2439
  const callerLocation = captureCallerLocation();
2372
2440
  const used = {
2373
2441
  method: originMethod,
2374
2442
  args: originArgs,
2375
2443
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
2376
2444
  };
2377
- sink.capturedLocators.push({
2378
- location: callerLocation,
2379
- used,
2380
- element: null,
2381
- alternatives: []
2382
- });
2445
+ const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
2446
+ let seq = -1;
2447
+ if (!alreadyProbed) {
2448
+ seq = sink.capturedLocators.length;
2449
+ sink.capturedLocators.push({
2450
+ location: callerLocation,
2451
+ used,
2452
+ element: null,
2453
+ alternatives: []
2454
+ });
2455
+ }
2383
2456
  let result;
2384
2457
  try {
2385
2458
  result = await fn.apply(target, callArgs);
@@ -2387,7 +2460,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
2387
2460
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
2388
2461
  throw error;
2389
2462
  }
2390
- startElementCapture(sink, target, seq, callerLocation, used);
2463
+ if (!alreadyProbed) {
2464
+ if (callerLocation) sink.probedLocations.add(callerLocation);
2465
+ startElementCapture(sink, page, target, seq, callerLocation, used);
2466
+ }
2391
2467
  return result;
2392
2468
  };
2393
2469
  }
@@ -2420,6 +2496,9 @@ function instrumentPage(page) {
2420
2496
  return wrapLocator(page, original(...args), method, args);
2421
2497
  };
2422
2498
  }
2499
+ if (typeof page.on === "function") {
2500
+ page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
2501
+ }
2423
2502
  }
2424
2503
  page.on("console", (msg) => {
2425
2504
  const sink = currentSink;
@@ -2484,6 +2563,10 @@ function instrumentPage(page) {
2484
2563
  function instrumentContext(context) {
2485
2564
  if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
2486
2565
  INSTRUMENTED_CONTEXTS.add(context);
2566
+ if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
2567
+ void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
2568
+ });
2569
+ }
2487
2570
  const originalNewPage = context.newPage.bind(context);
2488
2571
  context.newPage = async (...args) => {
2489
2572
  const page = await originalNewPage(...args);
@@ -83,6 +83,33 @@ declare const EXPECT_CAPTURE_EXPRESSIONS: ReadonlySet<string>;
83
83
  * Returns the first quoted string after the role, or null if none found.
84
84
  */
85
85
  declare function extractAccessibleName(ariaSnapshot: string | null): string | null;
86
+ /** Tags whose accessible name comes from an associated `<label>`. */
87
+ declare const FORM_FIELD_TAGS: ReadonlySet<string>;
88
+ /**
89
+ * Roles whose accessible name is computed from the element's own content. For
90
+ * these, an element carrying neither `aria-label` nor `aria-labelledby` is
91
+ * named by its trimmed text — which the probe already returns.
92
+ */
93
+ declare const NAME_FROM_CONTENT_ROLES: ReadonlySet<string>;
94
+ /** The subset of a probe result the accessible-name shortcut reads. */
95
+ interface NameSource {
96
+ tagName: string;
97
+ attributes: Record<string, string | null>;
98
+ textContent: string | null;
99
+ labelText?: string | null;
100
+ }
101
+ /**
102
+ * The accessible name when the probed attributes already settle it, or null
103
+ * when only the browser can. Capture consults this before spending an
104
+ * `ariaSnapshot()` round trip on an element: in each case below the name is
105
+ * what the accessible-name computation would return anyway, so the round trip
106
+ * would buy nothing.
107
+ *
108
+ * Deliberately narrow — anything outside these cases (an `aria-labelledby`
109
+ * reference, an image named by `alt`, an element with no text) still goes to
110
+ * the browser, because a wrong name here becomes a wrong `getByRole` locator.
111
+ */
112
+ declare function exactAccessibleName(attrs: NameSource, role: string | null): string | null;
86
113
  /** A failed locator action, used to suggest a fresh locator from the live page. */
87
114
  interface FailedLocatorInfo {
88
115
  method: string;
@@ -126,4 +153,4 @@ declare function suggestLocatorsFromAria(failed: FailedLocatorInfo, ariaSnapshot
126
153
  */
127
154
  declare function captureCallerLocation(stack?: string): string | null;
128
155
 
129
- export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, captureCallerLocation, dedupeSnapshotsByLocation, extractAccessibleName, renderFailing, suggestLocatorsFromAria };
156
+ export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, FORM_FIELD_TAGS, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, NAME_FROM_CONTENT_ROLES, type NameSource, captureCallerLocation, dedupeSnapshotsByLocation, exactAccessibleName, extractAccessibleName, renderFailing, suggestLocatorsFromAria };
@@ -35,14 +35,17 @@ __export(locator_healing_exports, {
35
35
  CHAIN_METHODS: () => CHAIN_METHODS,
36
36
  EXPECT_CAPTURE_EXPRESSIONS: () => EXPECT_CAPTURE_EXPRESSIONS,
37
37
  EXPECT_METHOD: () => EXPECT_METHOD,
38
+ FORM_FIELD_TAGS: () => FORM_FIELD_TAGS,
38
39
  INPUT_TYPE_TO_ROLE: () => INPUT_TYPE_TO_ROLE,
39
40
  LOCATOR_CREATING_CHAINS: () => LOCATOR_CREATING_CHAINS,
40
41
  LOCATOR_METHODS: () => LOCATOR_METHODS,
42
+ NAME_FROM_CONTENT_ROLES: () => NAME_FROM_CONTENT_ROLES,
41
43
  TAG_TO_ROLE: () => TAG_TO_ROLE,
42
44
  approximateAccessibleName: () => approximateAccessibleName,
43
45
  captureCallerLocation: () => captureCallerLocation,
44
46
  classifyCssStability: () => classifyCssStability,
45
47
  dedupeSnapshotsByLocation: () => dedupeSnapshotsByLocation,
48
+ exactAccessibleName: () => exactAccessibleName,
46
49
  extractAccessibleName: () => extractAccessibleName,
47
50
  generateAlternatives: () => generateAlternatives,
48
51
  headingLevel: () => headingLevel,
@@ -195,6 +198,7 @@ var CAPTURED_ATTRIBUTES = [
195
198
  "alt",
196
199
  "title",
197
200
  "aria-label",
201
+ "aria-labelledby",
198
202
  "aria-level",
199
203
  "role",
200
204
  "type",
@@ -573,6 +577,43 @@ function extractAccessibleName(ariaSnapshot) {
573
577
  if (match) return match[1];
574
578
  return null;
575
579
  }
580
+ var FORM_FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
581
+ var NAME_FROM_CONTENT_ROLES = /* @__PURE__ */ new Set([
582
+ "button",
583
+ "cell",
584
+ "checkbox",
585
+ "columnheader",
586
+ "gridcell",
587
+ "heading",
588
+ "link",
589
+ "menuitem",
590
+ "menuitemcheckbox",
591
+ "menuitemradio",
592
+ "option",
593
+ "radio",
594
+ "row",
595
+ "rowheader",
596
+ "switch",
597
+ "tab",
598
+ "tooltip",
599
+ "treeitem"
600
+ ]);
601
+ var PROBE_TEXT_CAP = 80;
602
+ var PROBE_LABEL_TEXT_CAP = 120;
603
+ function exactAccessibleName(attrs, role) {
604
+ if (attrs.attributes["aria-labelledby"]) return null;
605
+ const ariaLabel = attrs.attributes["aria-label"];
606
+ if (ariaLabel) return ariaLabel;
607
+ if (FORM_FIELD_TAGS.has(attrs.tagName)) {
608
+ const labelText = attrs.labelText;
609
+ return labelText && labelText.length < PROBE_LABEL_TEXT_CAP ? labelText : null;
610
+ }
611
+ if (role && NAME_FROM_CONTENT_ROLES.has(role)) {
612
+ const text = attrs.textContent;
613
+ return text && text.length < PROBE_TEXT_CAP ? text : null;
614
+ }
615
+ return null;
616
+ }
576
617
  var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
577
618
  "getByText",
578
619
  "getByRole",
@@ -707,14 +748,17 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
707
748
  CHAIN_METHODS,
708
749
  EXPECT_CAPTURE_EXPRESSIONS,
709
750
  EXPECT_METHOD,
751
+ FORM_FIELD_TAGS,
710
752
  INPUT_TYPE_TO_ROLE,
711
753
  LOCATOR_CREATING_CHAINS,
712
754
  LOCATOR_METHODS,
755
+ NAME_FROM_CONTENT_ROLES,
713
756
  TAG_TO_ROLE,
714
757
  approximateAccessibleName,
715
758
  captureCallerLocation,
716
759
  classifyCssStability,
717
760
  dedupeSnapshotsByLocation,
761
+ exactAccessibleName,
718
762
  extractAccessibleName,
719
763
  generateAlternatives,
720
764
  headingLevel,
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@piwitests/reporter",
3
- "version": "0.24.0",
3
+ "version": "0.26.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
- "homepage": "https://piwitests.github.io",
6
+ "homepage": "https://piwitests.dev",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/PiwiTests/platform"
@@ -53,6 +53,8 @@
53
53
  "reporter:test:integration": "npm run reporter:build && playwright test --config=tests/integration/playwright.config.ts",
54
54
  "reporter:test:integration:ai": "npm run reporter:build && node tests/integration/ai/run.mjs",
55
55
  "reporter:test:integration:ai:live": "npm run reporter:build && playwright test --config=tests/integration/ai/live.config.ts",
56
+ "reporter:bench": "npm run reporter:build && node tests/bench/run.mjs",
57
+ "reporter:bench:micro": "vitest bench --run",
56
58
  "test": "npm run reporter:test",
57
59
  "prepublishOnly": "npm run reporter:build"
58
60
  },
@@ -60,6 +62,9 @@
60
62
  "dist/",
61
63
  "templates/"
62
64
  ],
65
+ "engines": {
66
+ "node": ">=20"
67
+ },
63
68
  "peerDependencies": {
64
69
  "@playwright/test": "^1.61.1"
65
70
  },
@@ -5,7 +5,7 @@ description: Investigate a failed test run recorded in Piwi Dashboard and propos
5
5
 
6
6
  # Investigate a Piwi failure
7
7
 
8
- Turn a failed run in [Piwi Dashboard](https://piwitests.github.io) into a grounded diagnosis and a concrete fix. Piwi has already gathered the evidence — the error text, the steps that ran, console output, failing network calls, a suggested locator, and the source diff since the last passing run. Use that instead of guessing.
8
+ Turn a failed run in [Piwi Dashboard](https://piwitests.dev) into a grounded diagnosis and a concrete fix. Piwi has already gathered the evidence — the error text, the steps that ran, console output, failing network calls, a suggested locator, and the source diff since the last passing run. Use that instead of guessing.
9
9
 
10
10
  ## How you reach Piwi
11
11
 
@@ -0,0 +1,38 @@
1
+ ---
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.
4
+ ---
5
+
6
+ # Run the right tests with Piwi selections
7
+
8
+ Running the whole suite for every change is slow, and hand-maintained grep patterns go stale. A Piwi **selection** is a named, declarative subset of the suite that Piwi resolves from run history — smoke, critical-and-stable, recently-broken, the best five minutes. This skill picks the selection that fits the task and runs exactly those tests.
9
+
10
+ ## How you reach Piwi
11
+
12
+ Prefer the **Piwi MCP server** if it is connected (`list_selections`, `resolve_selection`, `preview_selection`). Otherwise use the reporter CLI (`npx @piwitests/reporter select|run`) with `PIWI_DASHBOARD_URL` / `PIWI_API_KEY` / `PIWI_PROJECT_NAME` set, or the dashboard's **Selections** tab.
13
+
14
+ ## Steps
15
+
16
+ 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:
17
+ - About to make a broad change, or want a quick confidence check → `smoke` (or `preview_selection` with `{ "include": [{ "tags": ["smoke"] }] }`).
18
+ - Iterating on a fix → `failed`, or a `{ "include": [{ "failedInLastRuns": 5 }] }` preview.
19
+ - Time-boxed → resolve any selection with a `budgetMs` (e.g. 300000 for five minutes).
20
+
21
+ 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.
22
+
23
+ 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:
24
+ ```
25
+ npx @piwitests/reporter run <key>
26
+ ```
27
+ Or run the materialized command `resolve_selection` returned directly with `playwright test`. Pass extra Playwright args after `--` (e.g. `-- --workers=4`).
28
+
29
+ 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.
30
+
31
+ 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.
32
+
33
+ ## Guardrails
34
+
35
+ - A selection that resolves to **zero** tests is never "nothing to do" — it means the definition is wrong. Stop and say so.
36
+ - Selections choose what to *run*; they never hide a failure. Quarantine is the tool for a test whose verdict should not block a merge — do not use a selection to route around a red test.
37
+ - Don't invent a selection key. Use `list_selections` to see the real ones, and `preview_selection` for an ad-hoc subset.
38
+ - The full suite stays the baseline. Selections are for the fast loops between full runs, not a permanent replacement.
@@ -5,7 +5,7 @@ description: Wire a Playwright project up to a Piwi Dashboard — install the re
5
5
 
6
6
  # Set up Piwi in a Playwright project
7
7
 
8
- Connect a Playwright test suite to a [Piwi Dashboard](https://piwitests.github.io) so every run is uploaded, kept, and analyzed. The mechanical work is done by a deterministic command; your job is to gather the right inputs, run it, finish anything it flags, and prove a run reaches the dashboard.
8
+ Connect a Playwright test suite to a [Piwi Dashboard](https://piwitests.dev) so every run is uploaded, kept, and analyzed. The mechanical work is done by a deterministic command; your job is to gather the right inputs, run it, finish anything it flags, and prove a run reaches the dashboard.
9
9
 
10
10
  ## Before you start
11
11