@piwitests/reporter 0.63.0 → 0.64.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.
@@ -1,6 +1,214 @@
1
- export { CAPTURED_ATTRIBUTES, INPUT_TYPE_TO_ROLE, TAG_TO_ROLE, approximateAccessibleName, classifyCssStability, generateAlternatives, headingLevel, isAutoGenerated, resolveAriaRole } from '@piwitests/core/locator-generation';
2
- import { LocatorSnapshot } from '@piwitests/core/locator-healing-types';
3
- export { AncestorAnchor, ElementAttributes, LocatorSnapshot, RankedLocator, RolePosition, SelectorCounts } from '@piwitests/core/locator-healing-types';
1
+ /**
2
+ * Shared types for locator healing — the in-runtime capture + storage of
3
+ * alternative locators for every element interaction.
4
+ *
5
+ * The single source of truth: the dashboard app re-exports these via
6
+ * `#shared/locator-healing.types`, and the reporter bundles them at build time.
7
+ */
8
+ /** A ranked alternative locator suggestion. */
9
+ interface RankedLocator {
10
+ locator: string;
11
+ method: string;
12
+ args: Record<string, unknown>;
13
+ /** 0-100 stability score. data-testid=100, semantic CSS=35-40, hash-suffixed=10. */
14
+ score: number;
15
+ /**
16
+ * True when a human confirmed this alternative in the reporter's
17
+ * failure-time locator picker (`pickLocatorOnFailure`). A confirmed pick is
18
+ * surfaced distinctly and preferred as the recommended fix.
19
+ */
20
+ pickedByUser?: boolean;
21
+ /** When the pick was saved from the dashboard's snapshot picker, ISO 8601. */
22
+ pickedAt?: string;
23
+ }
24
+ /**
25
+ * Match counts for candidate selectors, probed against the live DOM at capture
26
+ * time. A count > 1 marks the selector ambiguous (strict-mode violation).
27
+ */
28
+ interface SelectorCounts {
29
+ testId?: number;
30
+ id?: number;
31
+ name?: number;
32
+ classes?: Record<string, number>;
33
+ /** How many elements share this element's role *and* exact accessible name (`getByRole(role, { name })` without `exact` also matches names that contain it). Absent when unknown (an older capture, or a probe run without the structural pass). */
34
+ roleName?: number;
35
+ /** Of the `roleName` matches, how many are visible (a laid-out box, or an `offsetParent`) — what `getByRole(role, { name }).visible()` would match. Absent when unknown. */
36
+ visibleRoleName?: number;
37
+ /** How many elements `getByText` would match on this element's text, counted no further than 2. Absent when unknown. */
38
+ text?: number;
39
+ /** How many elements share this element's `placeholder` — what `getByPlaceholder` would really match. Absent when unknown. */
40
+ placeholder?: number;
41
+ /** How many elements share this element's `alt` — what `getByAltText` would really match. Absent when unknown. */
42
+ alt?: number;
43
+ /** How many elements share this element's `title` — what `getByTitle` would really match. Absent when unknown. */
44
+ title?: number;
45
+ }
46
+ /**
47
+ * The element's position among same-role elements in the document at capture
48
+ * time — a name-independent structural identity used to re-find a renamed
49
+ * element on the failing page.
50
+ */
51
+ interface RolePosition {
52
+ role: string;
53
+ /** How many elements resolved to this role, document-wide. */
54
+ count: number;
55
+ /** The captured element's 0-based index among them, in document order. */
56
+ index: number;
57
+ /** For headings: how many same-role elements share this element's level. */
58
+ levelCount?: number;
59
+ }
60
+ /**
61
+ * An anchor-worthy ancestor of the captured element — one carrying a stable
62
+ * hook (test id, id, explicit role, aria-label, or a landmark tag) that a
63
+ * scoped alternative locator can chain from. Nearest ancestors first.
64
+ */
65
+ interface AncestorAnchor {
66
+ tag: string;
67
+ /** Hops from the element (1 = direct parent). */
68
+ depth: number;
69
+ testId: string | null;
70
+ id: string | null;
71
+ /** Explicit role attribute, if any. */
72
+ role: string | null;
73
+ ariaLabel: string | null;
74
+ /** Same-role matches for the captured element within this ancestor. */
75
+ scopedRoleCount?: number;
76
+ /**
77
+ * Matches for the captured element's text within this ancestor. A role-less
78
+ * leaf (a price `<span>`, a status badge) has no role to scope, so this is
79
+ * what tells a chain that resolves to one element from one that does not.
80
+ */
81
+ scopedTextCount?: number;
82
+ /** Document-wide match count for this ancestor's own data-testid. */
83
+ testIdCount?: number;
84
+ /** Document-wide match count for this ancestor's own id. */
85
+ idCount?: number;
86
+ /** Document-wide count of elements resolving to this ancestor's landmark/explicit role. */
87
+ roleCount?: number;
88
+ /**
89
+ * A stable non-testid `data-*` hook on this ancestor (`data-product="43"`,
90
+ * `data-row-id="7"`). Many apps identify a repeated card or row this way and
91
+ * nothing else, so without it the only locator left is one that matches every
92
+ * card on the page.
93
+ */
94
+ dataAttr?: {
95
+ name: string;
96
+ value: string;
97
+ };
98
+ /** Document-wide match count for `dataAttr` as an attribute selector. */
99
+ dataAttrCount?: number;
100
+ /**
101
+ * Text inside this ancestor that tells it apart from its same-role siblings —
102
+ * the product name in a card, the customer name in a table row. A repeated
103
+ * container carries no unique hook of its own, so this is what makes
104
+ * `getByRole('listitem').filter({ hasText: 'Keyboard' })` land on one of them.
105
+ */
106
+ filterText?: string;
107
+ /** How many elements of this ancestor's role contain `filterText`. */
108
+ filterRoleCount?: number;
109
+ }
110
+ /** Raw element attributes captured after a successful action. */
111
+ interface ElementAttributes {
112
+ tagName: string;
113
+ attributes: Record<string, string | null>;
114
+ /** Visible text, truncated to 80 chars. */
115
+ textContent: string | null;
116
+ /** Browser-computed accessible name from ariaSnapshot(). */
117
+ accessibleName: string | null;
118
+ /** Center point of the element's bounding box — spatial discriminator. */
119
+ center: {
120
+ x: number;
121
+ y: number;
122
+ } | null;
123
+ /** True when the element has an associated <label> — gates getByLabel. */
124
+ hasLabel?: boolean;
125
+ /** Text of the elements `aria-labelledby` points at, else of the first associated `<label>` — what names a form field. Absent when the probe did not read it. */
126
+ labelText?: string | null;
127
+ /** Live-page uniqueness probe results for candidate selectors. */
128
+ selectorCounts?: SelectorCounts;
129
+ /** Position among same-role elements — powers name-free and renamed-element healing. */
130
+ rolePosition?: RolePosition | null;
131
+ /** Anchor-worthy ancestors, nearest first — power ancestor-scoped alternatives. */
132
+ ancestors?: AncestorAnchor[];
133
+ }
134
+ /** One captured element interaction (per locator call site). */
135
+ interface LocatorSnapshot {
136
+ /** The Playwright step location — unique, stable identity of this call site. */
137
+ location: string | null;
138
+ /** The locator the test code actually used. */
139
+ used: {
140
+ method: string;
141
+ /** All args passed to the locator method. */
142
+ args: unknown[];
143
+ /** Original step title, for round-tripping. */
144
+ raw: string;
145
+ };
146
+ /** Element attributes — null for gap entries (failed actions). */
147
+ element: {
148
+ tagName: string;
149
+ attributes: Record<string, string | null>;
150
+ textContent: string | null;
151
+ accessibleName: string | null;
152
+ center: {
153
+ x: number;
154
+ y: number;
155
+ } | null;
156
+ /** Position among same-role elements at capture time (newer reporters only). */
157
+ rolePosition?: RolePosition | null;
158
+ /** Anchor-worthy ancestors, nearest first (newer reporters only). */
159
+ ancestors?: AncestorAnchor[];
160
+ } | null;
161
+ /** Computed alternative locators, ranked by stability score (max 10). */
162
+ alternatives: RankedLocator[];
163
+ }
164
+
165
+ /**
166
+ * Locator generation — pure functions for building ranked alternative locators
167
+ * from element attributes. The single source of truth shared by the server, the
168
+ * dashboard SPA, the interactive DOM snapshot picker (which runs in the
169
+ * browser), and the Playwright reporter (which bundles this at build time).
170
+ */
171
+
172
+ /** Implicit ARIA role for an HTML tag (when no explicit `role` is set). */
173
+ declare const TAG_TO_ROLE: Record<string, string>;
174
+ /** Implicit ARIA role for an `<input>` keyed by its `type` attribute. */
175
+ declare const INPUT_TYPE_TO_ROLE: Record<string, string>;
176
+ /** Element attributes captured by the probe. */
177
+ declare const CAPTURED_ATTRIBUTES: string[];
178
+ declare function resolveAriaRole(attrs: ElementAttributes): string | null;
179
+ declare function headingLevel(attrs: ElementAttributes, role: string | null): number | null;
180
+ declare function classifyCssStability(className: string): number;
181
+ declare function isAutoGenerated(value: string): boolean;
182
+ interface GenerateOptions {
183
+ /**
184
+ * The element's role from an accessibility model, for a caller that has one:
185
+ * every role candidate uses it instead of `resolveAriaRole(attrs)`, and null
186
+ * means the element has none. `rolePosition` and the anchors' scoped role
187
+ * counts must then describe this role.
188
+ */
189
+ role?: string | null;
190
+ /**
191
+ * The attribute `getByTestId` reads in the project (Playwright's
192
+ * `testIdAttribute`); `data-testid` when null or absent. The element's
193
+ * `getByTestId` candidate comes from it, and so do the anchors', whose
194
+ * `testId` and `testIdCount` the probe reads from the same attribute
195
+ * (`ProbeArg.testIdAttribute`). No `locator()` candidate selects on it, since
196
+ * `getByTestId` does; any other attribute, `data-testid` included, is an
197
+ * ordinary one.
198
+ */
199
+ testIdAttribute?: string | null;
200
+ }
201
+ declare function generateAlternatives(attrs: ElementAttributes, options?: GenerateOptions): RankedLocator[];
202
+ /**
203
+ * Approximate the accessible name from probed attributes, in the order the
204
+ * accessible-name computation applies: `aria-labelledby` (resolved by the probe
205
+ * into `labelText`), `aria-label`, then — for a form field — its `<label>`, or —
206
+ * for anything else — its text, then `title` and `placeholder`.
207
+ *
208
+ * A form field is never named by its own text: a `<select>`'s text is its
209
+ * options and a `<textarea>`'s is its value.
210
+ */
211
+ declare function approximateAccessibleName(attrs: ElementAttributes): string | null;
4
212
 
5
213
  /**
6
214
  * Reporter-local locator healing.
@@ -157,4 +365,4 @@ declare function callSitePath(file: string, cwd?: string, windows?: boolean): st
157
365
  */
158
366
  declare function captureCallerLocation(stack?: string): string | null;
159
367
 
160
- export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, FORM_FIELD_TAGS, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, type NameSource, callSitePath, captureCallerLocation, dedupeSnapshotsByLocation, exactAccessibleName, extractAccessibleName, framePath, renderFailing, suggestLocatorsFromAria };
368
+ export { ACTION_METHODS, type AncestorAnchor, CAPTURED_ATTRIBUTES, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, type ElementAttributes, FORM_FIELD_TAGS, type FailedLocatorInfo, INPUT_TYPE_TO_ROLE, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSnapshot, type LocatorSuggestion, type NameSource, type RankedLocator, type RolePosition, type SelectorCounts, TAG_TO_ROLE, approximateAccessibleName, callSitePath, captureCallerLocation, classifyCssStability, dedupeSnapshotsByLocation, exactAccessibleName, extractAccessibleName, framePath, generateAlternatives, headingLevel, isAutoGenerated, renderFailing, resolveAriaRole, suggestLocatorsFromAria };
@@ -1,9 +1,187 @@
1
1
  import { TestInfo, Page } from '@playwright/test';
2
- import { PickedAnchorInfo, ProbeArg, ProbedAttrs } from '@piwitests/picker-dom';
3
- export { PickedAnchorInfo, PickedLeafInfo, generateAnchoredAlternatives, installPickerOverlay, mergeCandidates, showAnchorPicker, showPickerChoices } from '@piwitests/picker-dom';
4
- import { FailedLocatorInfo } from './locator-healing.js';
5
- import { RankedLocator, LocatorSnapshot } from '@piwitests/core/locator-healing-types';
6
- import '@piwitests/core/locator-generation';
2
+ import { P as ProbeArg, a as ProbedAttrs } from '../../types-CGr9jJZ_.js';
3
+ import { RankedLocator, LocatorSnapshot, FailedLocatorInfo } from './locator-healing.js';
4
+
5
+ /** The overlay's texts a host may give in its own language; each one left out is the English default. */
6
+ interface PickerOverlayStrings {
7
+ /** The banner's heading for the `'global'` transport with no failing locator. */
8
+ banner?: string;
9
+ /** The banner's foot before an element is hovered: the keys, Enter picking the focused element. */
10
+ keys?: string;
11
+ /** The banner's foot while an element is hovered: a click or Enter picks it. */
12
+ keysHovering?: string;
13
+ /** The banner's foot once an element is picked. */
14
+ analyzing?: string;
15
+ }
16
+ /** Configuration for `installPickerOverlay` — see the function doc for the transport split. */
17
+ interface PickerOverlayArg {
18
+ transport: 'global' | 'postMessage';
19
+ /** Rendered failing locator for the banner heading (`'global'` transport only). */
20
+ failing?: string | null;
21
+ /** Probe arguments for the inline probe the `'postMessage'` transport performs on pick. */
22
+ probeArg?: ProbeArg;
23
+ /** The banner's texts, for a host that shows another language than English. */
24
+ strings?: PickerOverlayStrings;
25
+ /**
26
+ * The attribute `getByTestId` reads in the project (Playwright's
27
+ * `testIdAttribute`); `data-testid` when null or absent. The overlay's own
28
+ * description of an element and its snapping to a test id read it.
29
+ */
30
+ testIdAttribute?: string | null;
31
+ }
32
+ /**
33
+ * Runs inside the browser — installs the element-picking overlay: a hover
34
+ * highlight, an instruction banner, and capture-phase listeners that suppress
35
+ * the app's own handlers while picking. The hover target snaps to the nearest
36
+ * actionable ancestor (button/link/field/role/test id) and up/down walk the
37
+ * DOM chain before the click commits. The hover reaches into open shadow
38
+ * roots, and the chain walks out of them through each host. Enter picks the
39
+ * outlined element, or with none the focused one, so a control reached with
40
+ * Tab is picked without the pointer.
41
+ *
42
+ * `transport` picks how a pick/skip is reported, since the two hosts cross
43
+ * fundamentally different boundaries:
44
+ * - `'global'` — the reporter drives a live page from Node over CDP. The
45
+ * picked element is parked on `globalThis.__piwiPickedElement` and
46
+ * `__piwiPickState` is set to `'picked'`/`'skipped'`, polled from Node;
47
+ * probing happens later, from Node, against a live element handle.
48
+ * - `'postMessage'` — the snapshot picker runs standalone inside a sandboxed
49
+ * opaque-origin iframe with nothing outside able to reach in, so the pick
50
+ * is probed and posted to `window.parent` in the same tick. The probe
51
+ * itself can't be imported (a serialized function carries no imports), so
52
+ * the embedding host installs it on `globalThis.__piwiProbe` before
53
+ * invoking this function, and may install lifecycle hooks on
54
+ * `globalThis.__piwiSnapshotExtras` (`onPick`/`onClose`) for its own
55
+ * snapshot-only chrome (search, highlight hints, extended inertness).
56
+ *
57
+ * The banner is in English unless `arg.strings` gives its texts.
58
+ *
59
+ * The hovered element's locator is shown twice — in a chip pinned to the
60
+ * element itself and on its own line in the banner. Both render whatever
61
+ * `globalThis.__piwiDescribeElement(el)` returns when a host installs one (the
62
+ * extension points it at the real ranked locator engine), falling back to this
63
+ * function's own attribute-order approximation. A host whose answer improves
64
+ * later (checked against the page once the pointer rests) calls
65
+ * `globalThis.__piwiRedescribe()` to have the hovered element described again.
66
+ *
67
+ * Must stay fully self-contained (no module-closure references) — both hosts
68
+ * re-serialize this function via `Function.prototype.toString()` (the
69
+ * reporter through `page.evaluate`, the snapshot picker through `String()`
70
+ * into a `<script>` tag).
71
+ */
72
+ declare function installPickerOverlay(arg: PickerOverlayArg): void;
73
+
74
+ /** An ancestor the human blessed as an anchor, with its in-page uniqueness counts. */
75
+ interface PickedAnchorInfo {
76
+ tag: string;
77
+ /** Hops from the picked element (1 = direct parent). */
78
+ depth: number;
79
+ testId: string | null;
80
+ id: string | null;
81
+ ariaLabel: string | null;
82
+ /** Resolved anchor role (explicit attribute or tag-implied). */
83
+ role: string | null;
84
+ /** Document-wide match count for the anchor's own test id. */
85
+ testIdCount?: number;
86
+ /** Document-wide match count for the anchor's own id. */
87
+ idCount?: number;
88
+ /** Document-wide count of same-role elements carrying the same aria-label. */
89
+ labeledRoleCount?: number;
90
+ /** Document-wide count of elements resolving to the anchor's role. */
91
+ roleCount?: number;
92
+ /** Leaf matches (picked element's identity) within this anchor's subtree. */
93
+ scopedLeafCount?: number;
94
+ }
95
+ /** The picked element's identity used for anchor-scoped candidates. */
96
+ interface PickedLeafInfo {
97
+ role: string | null;
98
+ level: number | null;
99
+ }
100
+ /**
101
+ * The anchors step's texts a host may give in its own language; each one left
102
+ * out is the English default. `{count}` in a text stands for a number.
103
+ */
104
+ interface AnchorPickerStrings {
105
+ title?: string;
106
+ hint?: string;
107
+ /** The footer with no parent selected. */
108
+ noneSelected?: string;
109
+ matchesOne?: string;
110
+ /** `{count}` elements, any count but one. */
111
+ matchesMany?: string;
112
+ countUnavailable?: string;
113
+ /** Under a parent: what it contains. */
114
+ containsOne?: string;
115
+ containsMany?: string;
116
+ /** Under a parent that cannot be selected. */
117
+ needsTestId?: string;
118
+ /** A parent with no test id, id or role, in place of its hook. */
119
+ noHook?: string;
120
+ use?: string;
121
+ skip?: string;
122
+ }
123
+ /** Arguments for `showAnchorPicker` — the role maps mirror the single source of truth in `@piwitests/core`. */
124
+ interface AnchorPickerArg {
125
+ tagRoles: Record<string, string>;
126
+ inputRoles: Record<string, string>;
127
+ roleSources: string;
128
+ leafRole: string;
129
+ leafLevel: number | null;
130
+ leafTestId: string | null;
131
+ /**
132
+ * The attribute `getByTestId` reads in the project (Playwright's
133
+ * `testIdAttribute`), which `leafTestId` and each parent's test id are read
134
+ * from; `data-testid` when null or absent.
135
+ */
136
+ testIdAttribute?: string | null;
137
+ /** The panel's texts, for a host that shows another language than English. */
138
+ strings?: AnchorPickerStrings;
139
+ }
140
+ /**
141
+ * Runs inside the browser via `evaluate()` — the anchor step: lists the picked
142
+ * element's ancestors so the human can bless one or more stable parents to
143
+ * scope the locator to. Each row shows the ancestor's strongest hook and how
144
+ * many leaf matches it contains; the footer shows a live "matches N" count for
145
+ * the combined selection, recomputed against the real page on every toggle
146
+ * (exactly 1 = green). Hovering a row outlines that ancestor in the page and
147
+ * names it in a chip pinned to it. Resolves through `__piwiAnchorState` ('done' | 'skipped'); selected anchors
148
+ * land in `__piwiPickAnchors` (+ `__piwiPickChainCount`). While the step
149
+ * shows, `__piwiAnchorCleanup` holds its teardown (see `removeAnchorPicker`).
150
+ * Role resolution reuses the maps passed in `arg` (single source of truth in
151
+ * `@piwitests/core`). The ancestors and every count reach through open shadow
152
+ * roots, as Playwright's locators do. The panel is in English unless
153
+ * `arg.strings` gives its texts. Must stay fully self-contained.
154
+ */
155
+ declare function showAnchorPicker(arg: AnchorPickerArg): void;
156
+
157
+ /** Arguments for `showPickerChoices`. */
158
+ interface PickerChoicesArg {
159
+ failing: string | null;
160
+ choices: Array<{
161
+ locator: string;
162
+ score: number;
163
+ }>;
164
+ }
165
+ /**
166
+ * Runs inside the browser via `evaluate()` — replaces the pick overlay with a
167
+ * confirmation panel listing the ranked replacement locators. The chosen index
168
+ * lands in `__piwiPickChoice` (-1 = skipped), polled from Node. While it shows,
169
+ * `__piwiChoiceCleanup` takes it out of the page unanswered (a pick that ended
170
+ * under it). Must stay fully self-contained.
171
+ */
172
+ declare function showPickerChoices(arg: PickerChoicesArg): void;
173
+
174
+ /**
175
+ * Build anchor-scoped candidates from the human-blessed parents. Shapes stay
176
+ * consistent with the heuristic chains in `generateAlternatives` — leaf method
177
+ * `getByRole` with flat anchor args, no leaf `name` key. Single-anchor
178
+ * candidates require the leaf to be unique inside that anchor; with two or
179
+ * more anchors a combined chain is added when the whole chain resolves to
180
+ * exactly one element (`chainLeafCount`). Pure — exported for tests.
181
+ */
182
+ declare function generateAnchoredAlternatives(leaf: PickedLeafInfo, anchors: PickedAnchorInfo[], chainLeafCount?: number): RankedLocator[];
183
+ /** Merge candidate lists, first list winning duplicates, sorted by score. Exported for tests. */
184
+ declare function mergeCandidates(base: RankedLocator[], extra: RankedLocator[]): RankedLocator[];
7
185
 
8
186
  /**
9
187
  * The probe dependency, passed in by the capture fixture to avoid an import
@@ -91,4 +269,4 @@ declare function runLocatorPicker(page: Page, testInfo: TestInfo, failed: Failed
91
269
  */
92
270
  declare function applyPickToSnapshots(snapshots: LocatorSnapshot[], pick: UserPickResult): boolean;
93
271
 
94
- export { type PickedLocator, type PickerProbe, type UserPickResult, applyPickToSnapshots, deriveFailedLocator, parseLeafLocatorExpression, pickLocator, runLocatorPicker, unlessEnded };
272
+ export { type PickedAnchorInfo, type PickedLeafInfo, type PickedLocator, type PickerProbe, type UserPickResult, applyPickToSnapshots, deriveFailedLocator, generateAnchoredAlternatives, installPickerOverlay, mergeCandidates, parseLeafLocatorExpression, pickLocator, runLocatorPicker, showAnchorPicker, showPickerChoices, unlessEnded };
@@ -0,0 +1,87 @@
1
+ /** Arguments for `probeElementAttrs` — the role maps mirror the single source of truth in `@piwitests/core`. */
2
+ interface ProbeArg {
3
+ /** Attribute whitelist to read off the element (the shared `CAPTURED_ATTRIBUTES`). */
4
+ keep: string[];
5
+ /** Required when `includeStructural` is true — role resolution needs the tag/input-type role maps. */
6
+ tagRoles?: Record<string, string>;
7
+ inputRoles?: Record<string, string>;
8
+ /** CSS selector matching every element role resolution can reach (also structural-only). */
9
+ roleSources?: string;
10
+ /** Compute `rolePosition` and ancestor-anchor candidates. The live picker always wants these; the snapshot picker never does (no anchors step there). */
11
+ includeStructural: boolean;
12
+ /**
13
+ * Count how many elements each candidate matches: `selectorCounts`,
14
+ * `rolePosition` and every anchor's counts, each a walk of the document or
15
+ * of an ancestor's subtree. True unless set: a caller that counts with an
16
+ * index of its own turns it off, and the anchors then carry what identifies
17
+ * them and no count, whatever the size of the page.
18
+ */
19
+ countMatches?: boolean;
20
+ /**
21
+ * The attribute `getByTestId` reads in the project (Playwright's
22
+ * `testIdAttribute`); `data-testid` when null or absent. It is read with
23
+ * `keep`, counted for `selectorCounts.testId`, read off each ancestor for its
24
+ * `testId` and `testIdCount`, and never taken as an ancestor's `dataAttr`;
25
+ * any other `data-*`, `data-testid` included, is an ordinary one.
26
+ */
27
+ testIdAttribute?: string | null;
28
+ }
29
+ /** Element shape the in-page probe returns — structural view of what the picker overlays need. */
30
+ interface ProbedAttrs {
31
+ tagName: string;
32
+ attributes: Record<string, string | null>;
33
+ textContent: string;
34
+ center: {
35
+ x: number;
36
+ y: number;
37
+ };
38
+ hasLabel: boolean;
39
+ /** Text of the elements `aria-labelledby` points at, else of the first associated `<label>`. */
40
+ labelText: string | null;
41
+ selectorCounts: {
42
+ testId?: number;
43
+ id?: number;
44
+ name?: number;
45
+ classes?: Record<string, number>;
46
+ /** How many elements share this element's role *and* exact accessible name (`getByRole(role, { name })` without `exact` also matches names that contain it). */
47
+ roleName?: number;
48
+ /** Of the `roleName` matches, how many are laid out (a box, or an `offsetParent`) — what `getByRole(role, { name }).visible()` would match. */
49
+ visibleRoleName?: number;
50
+ /** How many elements `getByText` would match on this element's text, counted no further than 2 — every consumer only asks whether that is exactly one. Undefined when the element has no text, or the subtree was too large to scan. */
51
+ text?: number;
52
+ /** How many elements share this element's `placeholder` — what `getByPlaceholder` would really match. */
53
+ placeholder?: number;
54
+ /** How many elements share this element's `alt` — what `getByAltText` would really match. */
55
+ alt?: number;
56
+ /** How many elements share this element's `title` — what `getByTitle` would really match. */
57
+ title?: number;
58
+ };
59
+ rolePosition?: {
60
+ role: string;
61
+ count: number;
62
+ index: number;
63
+ levelCount?: number;
64
+ } | null;
65
+ ancestors?: Array<{
66
+ tag: string;
67
+ depth: number;
68
+ testId: string | null;
69
+ id: string | null;
70
+ role: string | null;
71
+ ariaLabel: string | null;
72
+ scopedRoleCount?: number;
73
+ scopedTextCount?: number;
74
+ testIdCount?: number;
75
+ idCount?: number;
76
+ roleCount?: number;
77
+ dataAttr?: {
78
+ name: string;
79
+ value: string;
80
+ };
81
+ dataAttrCount?: number;
82
+ filterText?: string;
83
+ filterRoleCount?: number;
84
+ }>;
85
+ }
86
+
87
+ export type { ProbeArg as P, ProbedAttrs as a };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Wire leaf shapes — the small, identical building blocks of the JSON contract
3
+ * exchanged between the reporter and the server. The single source of truth
4
+ * shared by the app (`#shared/types`) and the reporter (`types/wire.ts`).
5
+ *
6
+ * The per-case *payloads* are intentionally NOT here: the reporter produces a
7
+ * loose `WireTestCase` superset while the server receives the stricter
8
+ * `TestCasePayload` / `StreamEventPayload`. Those live on each side and are kept
9
+ * field-compatible by `apps/application/tests/unit/wire-shared-drift.test.ts`.
10
+ */
11
+
12
+ /**
13
+ * One locator call and the page it ran on, as the capture fixtures record it
14
+ * in the `piwi-locator-pages` attachment: a test's calls, deduped by call
15
+ * site, chain and page. The dashboard joins it to the locator index, so a use
16
+ * of a chain knows the pages it was made on.
17
+ */
18
+ interface LocatorPageUse {
19
+ /** `file:line:col` of the call, as a step location reports it (absolute; the server makes it project-relative). */
20
+ location: string;
21
+ /** The chain as Playwright prints it (`Locator.toString()`). */
22
+ locator: string;
23
+ /** The page's origin (`https://shop.test`), to tell the application's pages from third-party ones. */
24
+ origin: string;
25
+ /** The page key: the path with ids and tokens collapsed, no query, no hash (`/orders/:id`). */
26
+ page: string;
27
+ /** No locator interaction happened on the page since it was navigated to: the element was there as it loaded. */
28
+ arrival: boolean;
29
+ }
30
+ /** What a process is for the run, from its command line. */
31
+ type ProcessRole = 'runner' | 'worker' | 'browser' | 'renderer' | 'gpu' | 'utility' | 'ffmpeg' | 'webServer' | 'other';
32
+ /**
33
+ * Name of the PNG the capture fixtures attach to a failed test: the page the
34
+ * test last acted on. The reporter uploads it with the execution's files, and
35
+ * the dashboard shows it as the page at the failure.
36
+ */
37
+ declare const FAILURE_SCREENSHOT_ATTACHMENT = "piwi-failure-screenshot";
38
+ /** What the processes of one role used during a test. */
39
+ interface RoleCost {
40
+ cpuMs: number;
41
+ /** Time runnable but waiting for a CPU. */
42
+ runWaitMs: number | null;
43
+ /** The largest process of the role during the test (its peak RSS). */
44
+ peakRssMb: number | null;
45
+ processes: number;
46
+ }
47
+
48
+ export { FAILURE_SCREENSHOT_ATTACHMENT as F, type LocatorPageUse as L, type ProcessRole as P, type RoleCost as R };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piwitests/reporter",
3
- "version": "0.63.0",
3
+ "version": "0.64.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/piwi",
6
6
  "homepage": "https://piwitests.dev",
@@ -66,7 +66,7 @@
66
66
  "node": ">=20"
67
67
  },
68
68
  "peerDependencies": {
69
- "@playwright/test": "^1.64.1"
69
+ "@playwright/test": ">=1.59.0 <2"
70
70
  },
71
71
  "dependencies": {
72
72
  "form-data": "^4.0.0"
@@ -87,10 +87,12 @@ view, and work from what it shows plus the repo.
87
87
 
88
88
  8. **Report back to Piwi.** Tell Piwi what you decided about the gap with
89
89
  `triage_gap` (Maintainer role or above on the project): `verb: "accept"` for the gap you
90
- wrote the test for, `covered-by` with the covering test's id when a test you
91
- found already covers it, or `dismiss` with the reason (`not-worth-testing`,
92
- `covered-elsewhere`, `wrong`) when the evidence did not hold. Each verdict counts toward the precision of the
93
- detector that raised the gap, so a detector that keeps being wrong is muted.
90
+ wrote the test for, `covered-by` with the test's id once a test written for the gap
91
+ has run, or `dismiss` when the gap needed no new test: `covered-elsewhere` with the id
92
+ of a test that already covered it, `not-worth-testing`, or `wrong` when the evidence did
93
+ not hold. Each verdict counts toward the precision of the detector that raised the gap
94
+ (`accept` and `covered-by` for it, `wrong` and `covered-elsewhere` against it), so a
95
+ detector that keeps being wrong is muted.
94
96
 
95
97
  ## Guardrails
96
98