@piwitests/reporter 0.62.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.
- package/README.md +1 -1
- package/dist/cli/index.js +76 -40
- package/dist/global-setup-module.js +88 -14
- package/dist/index.d.ts +27 -8
- package/dist/index.js +17501 -13740
- package/dist/internal/capture/attachments.d.ts +1 -1
- package/dist/internal/capture/capture-fixtures.d.ts +142 -7
- package/dist/internal/capture/capture-fixtures.js +4394 -1039
- package/dist/internal/capture/locator-healing.d.ts +222 -4
- package/dist/internal/capture/locator-healing.js +22 -10
- package/dist/internal/capture/pick-on-failure.d.ts +184 -6
- package/dist/internal/capture/pick-on-failure.js +29 -5
- package/dist/types-CGr9jJZ_.d.ts +87 -0
- package/dist/wire-7zsqlBoY.d.ts +48 -0
- package/package.json +2 -2
- package/templates/skills/write-the-missing-test/SKILL.md +6 -4
|
@@ -1,6 +1,214 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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.
|
|
@@ -131,6 +339,16 @@ declare function renderFailing(failed: FailedLocatorInfo): string;
|
|
|
131
339
|
* present (so the failure wasn't a rename), or when no candidate is confident.
|
|
132
340
|
*/
|
|
133
341
|
declare function suggestLocatorsFromAria(failed: FailedLocatorInfo, ariaSnapshot: string | null): LocatorSuggestion | null;
|
|
342
|
+
/**
|
|
343
|
+
* The local path a stack frame's file names: a `file://` URL (an ES-module suite's frames) through `fileURLToPath`,
|
|
344
|
+
* any other path as it is. `windows` reads the URL as Windows does. Null for a URL that names no local file.
|
|
345
|
+
*/
|
|
346
|
+
declare function framePath(file: string, windows?: boolean): string | null;
|
|
347
|
+
/**
|
|
348
|
+
* A stack frame's file relative to `cwd`, with forward slashes: the form Playwright writes a location in its error
|
|
349
|
+
* messages. A file on another drive stays absolute. Null for a URL that names no local file.
|
|
350
|
+
*/
|
|
351
|
+
declare function callSitePath(file: string, cwd?: string, windows?: boolean): string | null;
|
|
134
352
|
/**
|
|
135
353
|
* Capture the calling test's source location (`file:line:col`) from the current
|
|
136
354
|
* stack, for stamping onto a locator snapshot. The path is made cwd-relative so
|
|
@@ -147,4 +365,4 @@ declare function suggestLocatorsFromAria(failed: FailedLocatorInfo, ariaSnapshot
|
|
|
147
365
|
*/
|
|
148
366
|
declare function captureCallerLocation(stack?: string): string | null;
|
|
149
367
|
|
|
150
|
-
export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, FORM_FIELD_TAGS, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, type NameSource, captureCallerLocation, dedupeSnapshotsByLocation, exactAccessibleName, extractAccessibleName, 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 };
|
|
@@ -41,11 +41,13 @@ __export(locator_healing_exports, {
|
|
|
41
41
|
LOCATOR_METHODS: () => LOCATOR_METHODS,
|
|
42
42
|
TAG_TO_ROLE: () => TAG_TO_ROLE,
|
|
43
43
|
approximateAccessibleName: () => approximateAccessibleName,
|
|
44
|
+
callSitePath: () => callSitePath,
|
|
44
45
|
captureCallerLocation: () => captureCallerLocation,
|
|
45
46
|
classifyCssStability: () => classifyCssStability,
|
|
46
47
|
dedupeSnapshotsByLocation: () => dedupeSnapshotsByLocation,
|
|
47
48
|
exactAccessibleName: () => exactAccessibleName,
|
|
48
49
|
extractAccessibleName: () => extractAccessibleName,
|
|
50
|
+
framePath: () => framePath,
|
|
49
51
|
generateAlternatives: () => generateAlternatives,
|
|
50
52
|
headingLevel: () => headingLevel,
|
|
51
53
|
isAutoGenerated: () => isAutoGenerated,
|
|
@@ -55,6 +57,7 @@ __export(locator_healing_exports, {
|
|
|
55
57
|
});
|
|
56
58
|
module.exports = __toCommonJS(locator_healing_exports);
|
|
57
59
|
var path = __toESM(require("path"));
|
|
60
|
+
var import_node_url = require("url");
|
|
58
61
|
|
|
59
62
|
// ../core/src/locator-chain.ts
|
|
60
63
|
var LOCATING_METHODS = /* @__PURE__ */ new Set([
|
|
@@ -744,6 +747,20 @@ function suggestLocatorsFromAria(failed, ariaSnapshot) {
|
|
|
744
747
|
if (suggestions.length === 0) return null;
|
|
745
748
|
return { failing: renderFailing(failed), suggestions };
|
|
746
749
|
}
|
|
750
|
+
function framePath(file, windows = process.platform === "win32") {
|
|
751
|
+
if (!file.startsWith("file://")) return file;
|
|
752
|
+
try {
|
|
753
|
+
return (0, import_node_url.fileURLToPath)(file, { windows });
|
|
754
|
+
} catch {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function callSitePath(file, cwd = process.cwd(), windows = process.platform === "win32") {
|
|
759
|
+
const local = framePath(file, windows);
|
|
760
|
+
if (local === null) return null;
|
|
761
|
+
const paths = windows ? path.win32 : path.posix;
|
|
762
|
+
return paths.relative(cwd, local).split(paths.sep).join("/");
|
|
763
|
+
}
|
|
747
764
|
function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
748
765
|
const lines = stack.split("\n");
|
|
749
766
|
let prevWasCaptureModule = false;
|
|
@@ -756,13 +773,8 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
756
773
|
prevWasCaptureModule = false;
|
|
757
774
|
continue;
|
|
758
775
|
}
|
|
759
|
-
|
|
760
|
-
if (!file || file.startsWith("node:")) {
|
|
761
|
-
prevWasCaptureModule = false;
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
file = file.replace(/^file:\/\/\/?/, "");
|
|
765
|
-
if (!/\.[a-z]+$/i.test(file)) {
|
|
776
|
+
const file = m[2] ? framePath(m[2]) : null;
|
|
777
|
+
if (!file || file.startsWith("node:") || !/\.[a-z]+$/i.test(file)) {
|
|
766
778
|
prevWasCaptureModule = false;
|
|
767
779
|
continue;
|
|
768
780
|
}
|
|
@@ -785,11 +797,9 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
785
797
|
}
|
|
786
798
|
let rel = file;
|
|
787
799
|
try {
|
|
788
|
-
rel =
|
|
800
|
+
rel = callSitePath(file) ?? file;
|
|
789
801
|
} catch {
|
|
790
802
|
}
|
|
791
|
-
rel = rel.split(path.sep).join("/");
|
|
792
|
-
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
793
803
|
return `${rel}:${m[3]}:${m[4]}`;
|
|
794
804
|
}
|
|
795
805
|
return null;
|
|
@@ -807,11 +817,13 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
807
817
|
LOCATOR_METHODS,
|
|
808
818
|
TAG_TO_ROLE,
|
|
809
819
|
approximateAccessibleName,
|
|
820
|
+
callSitePath,
|
|
810
821
|
captureCallerLocation,
|
|
811
822
|
classifyCssStability,
|
|
812
823
|
dedupeSnapshotsByLocation,
|
|
813
824
|
exactAccessibleName,
|
|
814
825
|
extractAccessibleName,
|
|
826
|
+
framePath,
|
|
815
827
|
generateAlternatives,
|
|
816
828
|
headingLevel,
|
|
817
829
|
isAutoGenerated,
|
|
@@ -1,9 +1,187 @@
|
|
|
1
1
|
import { TestInfo, Page } from '@playwright/test';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 };
|
|
@@ -880,13 +880,17 @@ function showAnchorPicker(arg) {
|
|
|
880
880
|
}
|
|
881
881
|
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
882
882
|
const outline = doc.createElement("div");
|
|
883
|
+
outline.id = "__piwi_anchor_outline";
|
|
883
884
|
outline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #22c55e;background:rgba(34,197,94,.10);border-radius:4px;display:none;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(5,46,22,.5);`;
|
|
884
885
|
const outlineLabel = doc.createElement("div");
|
|
886
|
+
outlineLabel.id = "__piwi_anchor_label";
|
|
885
887
|
outlineLabel.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(420px,90vw);background:#0b1120;color:#f9fafb;border:1px solid #22c55e;border-radius:6px;padding:3px 7px;font:12px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
|
|
886
888
|
const pickedOutline = doc.createElement("div");
|
|
889
|
+
pickedOutline.id = "__piwi_anchor_picked";
|
|
887
890
|
const pr = el.getBoundingClientRect();
|
|
888
891
|
pickedOutline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55);left:${pr.left}px;top:${pr.top}px;width:${pr.width}px;height:${pr.height}px;`;
|
|
889
892
|
const panel = doc.createElement("div");
|
|
893
|
+
panel.id = "__piwi_anchor_panel";
|
|
890
894
|
panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:min(340px,calc(100vw - 56px));max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
|
|
891
895
|
const title = doc.createElement("div");
|
|
892
896
|
title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
|
|
@@ -1075,6 +1079,7 @@ function showPickerChoices(arg) {
|
|
|
1075
1079
|
}
|
|
1076
1080
|
const Z = 2147483600;
|
|
1077
1081
|
const wrap = doc.createElement("div");
|
|
1082
|
+
wrap.id = "__piwi_picker_choices";
|
|
1078
1083
|
wrap.style.cssText = `position:fixed;inset:0;z-index:${Z + 3};background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;font:13px/1.5 system-ui,sans-serif;`;
|
|
1079
1084
|
const panel = doc.createElement("div");
|
|
1080
1085
|
panel.style.cssText = "background:#111827;color:#f9fafb;border-radius:10px;padding:20px;max-width:640px;width:90vw;max-height:70vh;overflow:auto;box-shadow:0 8px 40px rgba(0,0,0,.5);";
|
|
@@ -1105,10 +1110,14 @@ function showPickerChoices(arg) {
|
|
|
1105
1110
|
}
|
|
1106
1111
|
panel.appendChild(title);
|
|
1107
1112
|
panel.appendChild(sub);
|
|
1108
|
-
const
|
|
1109
|
-
g.__piwiPickChoice = choice;
|
|
1113
|
+
const cleanup = () => {
|
|
1110
1114
|
doc.removeEventListener("keydown", onKey, true);
|
|
1111
1115
|
wrap.remove();
|
|
1116
|
+
if (g.__piwiChoiceCleanup === cleanup) delete g.__piwiChoiceCleanup;
|
|
1117
|
+
};
|
|
1118
|
+
const done = (choice) => {
|
|
1119
|
+
g.__piwiPickChoice = choice;
|
|
1120
|
+
cleanup();
|
|
1112
1121
|
};
|
|
1113
1122
|
const onKey = (e) => {
|
|
1114
1123
|
if (e.key !== "Escape") return;
|
|
@@ -1144,6 +1153,7 @@ function showPickerChoices(arg) {
|
|
|
1144
1153
|
});
|
|
1145
1154
|
panel.appendChild(skip);
|
|
1146
1155
|
doc.addEventListener("keydown", onKey, true);
|
|
1156
|
+
g.__piwiChoiceCleanup = cleanup;
|
|
1147
1157
|
wrap.appendChild(panel);
|
|
1148
1158
|
doc.body.appendChild(wrap);
|
|
1149
1159
|
}
|
|
@@ -1569,6 +1579,7 @@ function mergeCandidates(base, extra) {
|
|
|
1569
1579
|
|
|
1570
1580
|
// src/internal/capture/locator-healing.ts
|
|
1571
1581
|
var path = __toESM(require("path"));
|
|
1582
|
+
var import_node_url = require("url");
|
|
1572
1583
|
|
|
1573
1584
|
// ../core/src/locator-fingerprint.ts
|
|
1574
1585
|
var FUNCTION_WORDS = new Set(
|
|
@@ -1625,8 +1636,10 @@ async function cleanupPicker(page, ended = false) {
|
|
|
1625
1636
|
const g = globalThis;
|
|
1626
1637
|
if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
|
|
1627
1638
|
if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
|
|
1639
|
+
if (typeof g.__piwiChoiceCleanup === "function") g.__piwiChoiceCleanup();
|
|
1628
1640
|
delete g.__piwiPickCleanup;
|
|
1629
1641
|
delete g.__piwiAnchorCleanup;
|
|
1642
|
+
delete g.__piwiChoiceCleanup;
|
|
1630
1643
|
delete g.__piwiPickState;
|
|
1631
1644
|
delete g.__piwiPickChoice;
|
|
1632
1645
|
delete g.__piwiPickedElement;
|
|
@@ -1652,13 +1665,24 @@ function unlessEnded(run, ended) {
|
|
|
1652
1665
|
]);
|
|
1653
1666
|
}
|
|
1654
1667
|
async function pickLocator(page, probe, failing, ended) {
|
|
1668
|
+
let polling = false;
|
|
1655
1669
|
const answered = async (name) => {
|
|
1656
1670
|
const wait = page.waitForFunction(
|
|
1657
|
-
([key, endedKey]) =>
|
|
1671
|
+
([key, endedKey]) => {
|
|
1672
|
+
const g = globalThis;
|
|
1673
|
+
if (g[endedKey] !== true) return g[key] !== void 0;
|
|
1674
|
+
delete g[endedKey];
|
|
1675
|
+
return true;
|
|
1676
|
+
},
|
|
1658
1677
|
[name, PICK_ENDED],
|
|
1659
1678
|
{ timeout: 0, polling: 250 }
|
|
1660
1679
|
);
|
|
1661
|
-
|
|
1680
|
+
polling = true;
|
|
1681
|
+
const done = await unlessEnded(
|
|
1682
|
+
wait.then(() => true),
|
|
1683
|
+
ended
|
|
1684
|
+
);
|
|
1685
|
+
if (done) polling = false;
|
|
1662
1686
|
return !ended?.aborted;
|
|
1663
1687
|
};
|
|
1664
1688
|
try {
|
|
@@ -1727,7 +1751,7 @@ async function pickLocator(page, probe, failing, ended) {
|
|
|
1727
1751
|
...anchors.length > 0 ? { anchors } : {}
|
|
1728
1752
|
};
|
|
1729
1753
|
} finally {
|
|
1730
|
-
await cleanupPicker(page,
|
|
1754
|
+
await cleanupPicker(page, polling);
|
|
1731
1755
|
}
|
|
1732
1756
|
}
|
|
1733
1757
|
async function runLocatorPicker(page, testInfo, failed, probe) {
|