@piwitests/reporter 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/global-setup-module.d.ts +2 -1
- package/dist/global-setup-module.js +432 -3
- package/dist/index.d.ts +204 -8
- package/dist/index.js +4724 -23
- package/dist/internal/capture/attachments.d.ts +9 -3
- package/dist/internal/capture/attachments.js +46 -20
- package/dist/internal/capture/capture-fixtures.d.ts +110 -7
- package/dist/internal/capture/capture-fixtures.js +2227 -639
- package/dist/internal/capture/inspect-on-failure.d.ts +50 -0
- package/dist/internal/capture/inspect-on-failure.js +66 -0
- package/dist/internal/capture/locator-healing.d.ts +33 -131
- package/dist/internal/capture/locator-healing.js +602 -711
- package/dist/internal/capture/pick-on-failure.d.ts +198 -0
- package/dist/internal/capture/pick-on-failure.js +1203 -0
- package/package.json +9 -4
- package/dist/internal/collect/error-text.d.ts +0 -18
- package/dist/internal/collect/error-text.js +0 -79
- package/dist/internal/collect/metadata-collector.d.ts +0 -32
- package/dist/internal/collect/metadata-collector.js +0 -246
- package/dist/internal/collect/skip-classify.d.ts +0 -27
- package/dist/internal/collect/skip-classify.js +0 -40
- package/dist/internal/collect/step-analyzer.d.ts +0 -103
- package/dist/internal/collect/step-analyzer.js +0 -221
- package/dist/internal/config/env.d.ts +0 -45
- package/dist/internal/config/env.js +0 -153
- package/dist/internal/files/compression.d.ts +0 -5
- package/dist/internal/files/compression.js +0 -69
- package/dist/internal/files/file-handler.d.ts +0 -38
- package/dist/internal/files/file-handler.js +0 -198
- package/dist/internal/streaming/crash-recovery.d.ts +0 -23
- package/dist/internal/streaming/crash-recovery.js +0 -106
- package/dist/internal/streaming/stream-buffer.d.ts +0 -17
- package/dist/internal/streaming/stream-buffer.js +0 -102
- package/dist/internal/streaming/stream-manager.d.ts +0 -88
- package/dist/internal/streaming/stream-manager.js +0 -395
- package/dist/internal/submit/run-submitter.d.ts +0 -67
- package/dist/internal/submit/run-submitter.js +0 -190
- package/dist/internal/submit/serializer.d.ts +0 -45
- package/dist/internal/submit/serializer.js +0 -107
- package/dist/internal/submit/uploader.d.ts +0 -89
- package/dist/internal/submit/uploader.js +0 -226
- package/dist/internal/support/ci.d.ts +0 -2
- package/dist/internal/support/ci.js +0 -32
- package/dist/internal/support/cli-filters.d.ts +0 -1
- package/dist/internal/support/cli-filters.js +0 -51
- package/dist/internal/support/errors.d.ts +0 -8
- package/dist/internal/support/errors.js +0 -15
- package/dist/internal/support/instance-id.d.ts +0 -4
- package/dist/internal/support/instance-id.js +0 -48
- package/dist/internal/support/limiter.d.ts +0 -2
- package/dist/internal/support/limiter.js +0 -27
- package/dist/internal/support/logger.d.ts +0 -26
- package/dist/internal/support/logger.js +0 -43
- package/dist/internal/support/reporter-version.d.ts +0 -2
- package/dist/internal/support/reporter-version.js +0 -54
- package/dist/internal/support/setup-file.d.ts +0 -13
- package/dist/internal/support/setup-file.js +0 -61
- package/dist/internal/support/source-snippet.d.ts +0 -12
- package/dist/internal/support/source-snippet.js +0 -97
- package/dist/internal/support/worker-index.d.ts +0 -7
- package/dist/internal/support/worker-index.js +0 -14
- package/dist/internal/transport/http-client.d.ts +0 -52
- package/dist/internal/transport/http-client.js +0 -201
- package/dist/public/config-wrapper.d.ts +0 -21
- package/dist/public/config-wrapper.js +0 -64
- package/dist/public/global-setup.d.ts +0 -13
- package/dist/public/global-setup.js +0 -146
- package/dist/public/options.d.ts +0 -78
- package/dist/public/options.js +0 -2
- package/dist/public/reporter.d.ts +0 -68
- package/dist/public/reporter.js +0 -376
- package/dist/types/collected.d.ts +0 -96
- package/dist/types/collected.js +0 -10
- package/dist/types/wire.d.ts +0 -174
- package/dist/types/wire.js +0 -14
- package/dist/types.d.ts +0 -11
- package/dist/types.js +0 -27
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/internal/capture/pick-on-failure.ts
|
|
31
|
+
var pick_on_failure_exports = {};
|
|
32
|
+
__export(pick_on_failure_exports, {
|
|
33
|
+
applyPickToSnapshots: () => applyPickToSnapshots,
|
|
34
|
+
deriveFailedLocator: () => deriveFailedLocator,
|
|
35
|
+
generateAnchoredAlternatives: () => generateAnchoredAlternatives,
|
|
36
|
+
installPickerOverlay: () => installPickerOverlay,
|
|
37
|
+
mergeCandidates: () => mergeCandidates,
|
|
38
|
+
parseLeafLocatorExpression: () => parseLeafLocatorExpression,
|
|
39
|
+
runLocatorPicker: () => runLocatorPicker,
|
|
40
|
+
showAnchorPicker: () => showAnchorPicker,
|
|
41
|
+
showPickerChoices: () => showPickerChoices
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(pick_on_failure_exports);
|
|
44
|
+
var path2 = __toESM(require("path"));
|
|
45
|
+
|
|
46
|
+
// src/internal/capture/locator-healing.ts
|
|
47
|
+
var path = __toESM(require("path"));
|
|
48
|
+
|
|
49
|
+
// ../packages/core/src/locator-methods.ts
|
|
50
|
+
var LOCATOR_BUILDER_METHODS = [
|
|
51
|
+
"getByRole",
|
|
52
|
+
"getByTestId",
|
|
53
|
+
"getByText",
|
|
54
|
+
"getByLabel",
|
|
55
|
+
"getByPlaceholder",
|
|
56
|
+
"getByAltText",
|
|
57
|
+
"getByTitle",
|
|
58
|
+
"locator"
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// ../packages/core/src/locator-generation.ts
|
|
62
|
+
var TAG_TO_ROLE = {
|
|
63
|
+
a: "link",
|
|
64
|
+
button: "button",
|
|
65
|
+
nav: "navigation",
|
|
66
|
+
main: "main",
|
|
67
|
+
article: "article",
|
|
68
|
+
section: "region",
|
|
69
|
+
form: "form",
|
|
70
|
+
img: "img",
|
|
71
|
+
figure: "figure",
|
|
72
|
+
figcaption: "caption",
|
|
73
|
+
blockquote: "blockquote",
|
|
74
|
+
table: "table",
|
|
75
|
+
ul: "list",
|
|
76
|
+
ol: "list",
|
|
77
|
+
li: "listitem",
|
|
78
|
+
dialog: "dialog",
|
|
79
|
+
output: "status",
|
|
80
|
+
progress: "progressbar",
|
|
81
|
+
meter: "meter",
|
|
82
|
+
textarea: "textbox",
|
|
83
|
+
h1: "heading",
|
|
84
|
+
h2: "heading",
|
|
85
|
+
h3: "heading",
|
|
86
|
+
h4: "heading",
|
|
87
|
+
h5: "heading",
|
|
88
|
+
h6: "heading",
|
|
89
|
+
details: "group",
|
|
90
|
+
summary: "button",
|
|
91
|
+
search: "search"
|
|
92
|
+
};
|
|
93
|
+
var INPUT_TYPE_TO_ROLE = {
|
|
94
|
+
button: "button",
|
|
95
|
+
submit: "button",
|
|
96
|
+
reset: "button",
|
|
97
|
+
image: "button",
|
|
98
|
+
checkbox: "checkbox",
|
|
99
|
+
radio: "radio",
|
|
100
|
+
range: "slider",
|
|
101
|
+
search: "searchbox",
|
|
102
|
+
number: "spinbutton",
|
|
103
|
+
text: "textbox",
|
|
104
|
+
email: "textbox",
|
|
105
|
+
tel: "textbox",
|
|
106
|
+
url: "textbox",
|
|
107
|
+
password: "textbox"
|
|
108
|
+
};
|
|
109
|
+
function resolveAriaRole(attrs) {
|
|
110
|
+
const explicit = attrs.attributes["role"];
|
|
111
|
+
if (explicit) return explicit;
|
|
112
|
+
const tag = attrs.tagName;
|
|
113
|
+
if (!tag) return null;
|
|
114
|
+
if (tag === "input") {
|
|
115
|
+
const type = (attrs.attributes["type"] ?? "text").toLowerCase();
|
|
116
|
+
return INPUT_TYPE_TO_ROLE[type] ?? "textbox";
|
|
117
|
+
}
|
|
118
|
+
if (tag === "select") {
|
|
119
|
+
return attrs.attributes["multiple"] != null ? "listbox" : "combobox";
|
|
120
|
+
}
|
|
121
|
+
if (tag === "a") {
|
|
122
|
+
return attrs.attributes["href"] != null ? "link" : null;
|
|
123
|
+
}
|
|
124
|
+
return TAG_TO_ROLE[tag] ?? null;
|
|
125
|
+
}
|
|
126
|
+
function headingLevel(attrs, role) {
|
|
127
|
+
if (role !== "heading") return null;
|
|
128
|
+
const tagMatch = attrs.tagName.match(/^h([1-6])$/);
|
|
129
|
+
if (tagMatch) return Number(tagMatch[1]);
|
|
130
|
+
const ariaLevel = attrs.attributes["aria-level"];
|
|
131
|
+
if (ariaLevel && /^\d+$/.test(ariaLevel)) return Number(ariaLevel);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
var attr = (a, key) => a.attributes[key] || null;
|
|
135
|
+
var esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
136
|
+
var escCssAttrValue = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
137
|
+
var isCssSafeId = (id) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(id);
|
|
138
|
+
function classifyCssStability(className) {
|
|
139
|
+
if (/[a-f0-9]{8,}/i.test(className)) return 10;
|
|
140
|
+
if (/^(?:css|sc|emotion|styled)-/i.test(className)) return 15;
|
|
141
|
+
if (/^(bg|text|border|shadow|opacity|font|w-|h-|m[tblrxy]?-|p[tblrxy]?-|flex|grid|gap|rounded|absolute|relative|fixed|sticky|block|inline|hidden|overflow|z-|top-|right-|bottom-|left-|inset-|justify-|items-|self-|content-|order-|col-|row-)/.test(
|
|
142
|
+
className
|
|
143
|
+
))
|
|
144
|
+
return 25;
|
|
145
|
+
if (/__/.test(className) || /--/.test(className)) return 35;
|
|
146
|
+
if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
|
|
147
|
+
if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
|
|
148
|
+
if (className.includes("_")) return 15;
|
|
149
|
+
if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
|
|
150
|
+
return 15;
|
|
151
|
+
}
|
|
152
|
+
function isAutoGenerated(value) {
|
|
153
|
+
if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
|
|
154
|
+
if (/^[a-f0-9]{8,}$/i.test(value)) return true;
|
|
155
|
+
if (/^[a-z]+-\d+$/.test(value)) return true;
|
|
156
|
+
if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
|
|
157
|
+
if (value.startsWith("ng-")) return true;
|
|
158
|
+
if (/^(radix-|headlessui-|mui-|mantine-|chakra-)/i.test(value)) return true;
|
|
159
|
+
if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
function generateAlternatives(attrs) {
|
|
163
|
+
const alts = [];
|
|
164
|
+
const seen = /* @__PURE__ */ new Set();
|
|
165
|
+
const add = (loc) => {
|
|
166
|
+
if (!seen.has(loc.locator)) {
|
|
167
|
+
seen.add(loc.locator);
|
|
168
|
+
alts.push(loc);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const { accessibleName } = attrs;
|
|
172
|
+
const tag = attrs.tagName;
|
|
173
|
+
const role = resolveAriaRole(attrs);
|
|
174
|
+
const counts = attrs.selectorCounts;
|
|
175
|
+
const isUnique = (n) => n == null || n <= 1;
|
|
176
|
+
const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
|
|
177
|
+
const testId = attr(attrs, "data-testid");
|
|
178
|
+
if (testId && isUnique(counts?.testId)) {
|
|
179
|
+
add({
|
|
180
|
+
locator: `getByTestId('${esc(testId)}')`,
|
|
181
|
+
method: "getByTestId",
|
|
182
|
+
args: { testId },
|
|
183
|
+
score: 100
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const level = headingLevel(attrs, role);
|
|
187
|
+
const levelPart = level != null ? `, level: ${level}` : "";
|
|
188
|
+
const withLevel = (base) => level != null ? { ...base, level } : base;
|
|
189
|
+
if (role && accessibleName) {
|
|
190
|
+
add({
|
|
191
|
+
locator: `getByRole('${role}', { name: '${esc(accessibleName)}'${levelPart} })`,
|
|
192
|
+
method: "getByRole",
|
|
193
|
+
args: withLevel({ role, name: accessibleName }),
|
|
194
|
+
score: 90
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const ariaLabel = attr(attrs, "aria-label");
|
|
198
|
+
if (role && ariaLabel && ariaLabel !== accessibleName) {
|
|
199
|
+
add({
|
|
200
|
+
locator: `getByRole('${role}', { name: '${esc(ariaLabel)}'${levelPart} })`,
|
|
201
|
+
method: "getByRole",
|
|
202
|
+
args: withLevel({ role, name: ariaLabel }),
|
|
203
|
+
score: 85
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (accessibleName && ["input", "select", "textarea"].includes(tag)) {
|
|
207
|
+
const label = attr(attrs, "aria-label");
|
|
208
|
+
const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
|
|
209
|
+
if (labelBacked) {
|
|
210
|
+
add({
|
|
211
|
+
locator: `getByLabel('${esc(accessibleName)}')`,
|
|
212
|
+
method: "getByLabel",
|
|
213
|
+
args: { label: accessibleName },
|
|
214
|
+
score: 85
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const placeholder = attr(attrs, "placeholder");
|
|
219
|
+
if (placeholder) {
|
|
220
|
+
add({
|
|
221
|
+
locator: `getByPlaceholder('${esc(placeholder)}')`,
|
|
222
|
+
method: "getByPlaceholder",
|
|
223
|
+
args: { placeholder },
|
|
224
|
+
score: 80
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (text && text.length < 80) {
|
|
228
|
+
add({
|
|
229
|
+
locator: `getByText('${esc(text)}')`,
|
|
230
|
+
method: "getByText",
|
|
231
|
+
args: { text },
|
|
232
|
+
score: 75
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const id = attr(attrs, "id");
|
|
236
|
+
if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
237
|
+
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
238
|
+
add({
|
|
239
|
+
locator: `locator('${esc(selector)}')`,
|
|
240
|
+
method: "locator",
|
|
241
|
+
args: { selector },
|
|
242
|
+
score: 65
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const name = attr(attrs, "name");
|
|
246
|
+
if (name && isUnique(counts?.name)) {
|
|
247
|
+
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
248
|
+
add({
|
|
249
|
+
locator: `locator('${esc(selector)}')`,
|
|
250
|
+
method: "locator",
|
|
251
|
+
args: { selector },
|
|
252
|
+
score: 60
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const alt = attr(attrs, "alt");
|
|
256
|
+
if (alt) {
|
|
257
|
+
add({
|
|
258
|
+
locator: `getByAltText('${esc(alt)}')`,
|
|
259
|
+
method: "getByAltText",
|
|
260
|
+
args: { text: alt },
|
|
261
|
+
score: 60
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
const title = attr(attrs, "title");
|
|
265
|
+
if (title) {
|
|
266
|
+
add({
|
|
267
|
+
locator: `getByTitle('${esc(title)}')`,
|
|
268
|
+
method: "getByTitle",
|
|
269
|
+
args: { title },
|
|
270
|
+
score: 50
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const hasOwnTestId = !!(testId && isUnique(counts?.testId));
|
|
274
|
+
if (role && !hasOwnTestId) {
|
|
275
|
+
const rolePart = level != null ? `'${role}', { level: ${level} }` : `'${role}'`;
|
|
276
|
+
const leafArgs = withLevel({ role });
|
|
277
|
+
let testIdAnchorDone = false;
|
|
278
|
+
let idAnchorDone = false;
|
|
279
|
+
let roleAnchorDone = false;
|
|
280
|
+
for (const anc of attrs.ancestors ?? []) {
|
|
281
|
+
if (anc.scopedRoleCount !== 1) continue;
|
|
282
|
+
if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
|
|
283
|
+
testIdAnchorDone = true;
|
|
284
|
+
add({
|
|
285
|
+
locator: `getByTestId('${esc(anc.testId)}').getByRole(${rolePart})`,
|
|
286
|
+
method: "getByRole",
|
|
287
|
+
args: { ...leafArgs, anchorTestId: anc.testId },
|
|
288
|
+
score: 72
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
292
|
+
idAnchorDone = true;
|
|
293
|
+
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
294
|
+
add({
|
|
295
|
+
locator: `locator('${esc(anchorSelector)}').getByRole(${rolePart})`,
|
|
296
|
+
method: "getByRole",
|
|
297
|
+
args: { ...leafArgs, anchorSelector },
|
|
298
|
+
score: 64
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
|
|
302
|
+
if (!roleAnchorDone && ancestorRole && ancestorRole !== role && anc.roleCount === 1) {
|
|
303
|
+
roleAnchorDone = true;
|
|
304
|
+
add({
|
|
305
|
+
locator: `getByRole('${esc(ancestorRole)}').getByRole(${rolePart})`,
|
|
306
|
+
method: "getByRole",
|
|
307
|
+
args: { ...leafArgs, anchorRole: ancestorRole },
|
|
308
|
+
score: 55
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const pos = attrs.rolePosition;
|
|
313
|
+
if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
|
|
314
|
+
add({
|
|
315
|
+
locator: `getByRole(${rolePart})`,
|
|
316
|
+
method: "getByRole",
|
|
317
|
+
args: leafArgs,
|
|
318
|
+
score: 58
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const clsStr = attr(attrs, "class");
|
|
323
|
+
if (clsStr) {
|
|
324
|
+
const countsClasses = counts?.classes;
|
|
325
|
+
const classes = clsStr.split(/\s+/).filter((c) => c.length > 1 && /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(c) && isUnique(countsClasses?.[c])).map((cls) => ({ cls, score: classifyCssStability(cls) })).sort((a, b) => b.score - a.score).slice(0, 3);
|
|
326
|
+
for (const { cls, score } of classes) {
|
|
327
|
+
add({
|
|
328
|
+
locator: `locator('.${esc(cls)}')`,
|
|
329
|
+
method: "locator",
|
|
330
|
+
args: { selector: `.${cls}` },
|
|
331
|
+
score
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return alts.sort((a, b) => b.score - a.score);
|
|
336
|
+
}
|
|
337
|
+
function approximateAccessibleName(attrs) {
|
|
338
|
+
const a = attrs.attributes;
|
|
339
|
+
const ariaLabel = a["aria-label"];
|
|
340
|
+
if (ariaLabel) return ariaLabel;
|
|
341
|
+
if (attrs.textContent) return attrs.textContent;
|
|
342
|
+
const title = a["title"];
|
|
343
|
+
if (title) return title;
|
|
344
|
+
const placeholder = a["placeholder"];
|
|
345
|
+
if (placeholder) return placeholder;
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/internal/capture/locator-healing.ts
|
|
350
|
+
var LOCATOR_METHODS = [...LOCATOR_BUILDER_METHODS];
|
|
351
|
+
var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
|
|
352
|
+
var escAttr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
353
|
+
function failedNameAndRole(failed) {
|
|
354
|
+
if (failed.method === "getByRole") {
|
|
355
|
+
const role = typeof failed.args[0] === "string" ? failed.args[0] : null;
|
|
356
|
+
const opts = failed.args[1];
|
|
357
|
+
const name = opts && typeof opts.name === "string" ? opts.name : null;
|
|
358
|
+
const level = opts && typeof opts.level === "number" ? opts.level : null;
|
|
359
|
+
return { role, name, level };
|
|
360
|
+
}
|
|
361
|
+
const first = failed.args.find((a) => typeof a === "string");
|
|
362
|
+
return { role: null, name: typeof first === "string" ? first : null, level: null };
|
|
363
|
+
}
|
|
364
|
+
function renderFailing(failed) {
|
|
365
|
+
const { role, name } = failedNameAndRole(failed);
|
|
366
|
+
if (failed.method === "getByRole") {
|
|
367
|
+
return name ? `getByRole('${escAttr(role ?? "")}', { name: '${escAttr(name)}' })` : `getByRole('${escAttr(role ?? "")}')`;
|
|
368
|
+
}
|
|
369
|
+
return `${failed.method}('${escAttr(name ?? "")}')`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/internal/capture/pick-on-failure.ts
|
|
373
|
+
var ANSI_RE = /\[[0-9;]*m/g;
|
|
374
|
+
function endOfString(s, start) {
|
|
375
|
+
const q = s[start];
|
|
376
|
+
for (let i = start + 1; i < s.length; i++) {
|
|
377
|
+
if (s[i] === "\\") {
|
|
378
|
+
i++;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (s[i] === q) return i;
|
|
382
|
+
}
|
|
383
|
+
return s.length - 1;
|
|
384
|
+
}
|
|
385
|
+
function matchBrace(s, start) {
|
|
386
|
+
let depth = 0;
|
|
387
|
+
for (let i = start; i < s.length; i++) {
|
|
388
|
+
if (s[i] === "{") depth++;
|
|
389
|
+
else if (s[i] === "}" && --depth === 0) return i;
|
|
390
|
+
}
|
|
391
|
+
return s.length - 1;
|
|
392
|
+
}
|
|
393
|
+
function parseOptions(src) {
|
|
394
|
+
const obj = {};
|
|
395
|
+
const re = /(\w+)\s*:\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*"|true|false|-?\d+)/g;
|
|
396
|
+
let m;
|
|
397
|
+
while ((m = re.exec(src)) !== null) {
|
|
398
|
+
const key = m[1];
|
|
399
|
+
const raw = m[2];
|
|
400
|
+
if (raw === "true") obj[key] = true;
|
|
401
|
+
else if (raw === "false") obj[key] = false;
|
|
402
|
+
else if (/^-?\d+$/.test(raw)) obj[key] = Number(raw);
|
|
403
|
+
else obj[key] = raw.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
404
|
+
}
|
|
405
|
+
return obj;
|
|
406
|
+
}
|
|
407
|
+
function parseArgs(inner) {
|
|
408
|
+
const args = [];
|
|
409
|
+
let i = 0;
|
|
410
|
+
while (i < inner.length) {
|
|
411
|
+
const c = inner[i];
|
|
412
|
+
if (c === " " || c === ",") {
|
|
413
|
+
i++;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (c === "'" || c === '"') {
|
|
417
|
+
const end = endOfString(inner, i);
|
|
418
|
+
args.push(inner.slice(i + 1, end).replace(/\\(.)/g, "$1"));
|
|
419
|
+
i = end + 1;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (c === "{") {
|
|
423
|
+
const end = matchBrace(inner, i);
|
|
424
|
+
args.push(parseOptions(inner.slice(i, end + 1)));
|
|
425
|
+
i = end + 1;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
i++;
|
|
429
|
+
}
|
|
430
|
+
return args;
|
|
431
|
+
}
|
|
432
|
+
function leafExpression(expr) {
|
|
433
|
+
let depth = 0;
|
|
434
|
+
let leafStart = 0;
|
|
435
|
+
for (let i = 0; i < expr.length - 1; i++) {
|
|
436
|
+
const c = expr[i];
|
|
437
|
+
if (c === "'" || c === '"') {
|
|
438
|
+
i = endOfString(expr, i);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (c === "(") depth++;
|
|
442
|
+
else if (c === ")") {
|
|
443
|
+
depth--;
|
|
444
|
+
if (depth === 0 && expr[i + 1] === ".") leafStart = i + 2;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return expr.slice(leafStart);
|
|
448
|
+
}
|
|
449
|
+
function parseLeafLocatorExpression(rawExpr) {
|
|
450
|
+
const expr = leafExpression(rawExpr.trim());
|
|
451
|
+
const m = /^([A-Za-z]+)\((.*)\)$/s.exec(expr);
|
|
452
|
+
if (!m) return null;
|
|
453
|
+
return { method: m[1], args: parseArgs(m[2].trim()) };
|
|
454
|
+
}
|
|
455
|
+
function deriveFailedLocator(testInfo) {
|
|
456
|
+
const info = testInfo;
|
|
457
|
+
const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
|
|
458
|
+
for (const err of errors) {
|
|
459
|
+
const text = `${err.message ?? ""}
|
|
460
|
+
${err.stack ?? ""}`.replace(ANSI_RE, "");
|
|
461
|
+
const line = /^\s*Locator:\s*(.+)$/m.exec(text);
|
|
462
|
+
if (!line) continue;
|
|
463
|
+
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
464
|
+
if (!parsed) continue;
|
|
465
|
+
const loc = err.location;
|
|
466
|
+
const location = loc ? `${path2.relative(process.cwd(), loc.file).split(path2.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
467
|
+
return { method: parsed.method, args: parsed.args, location };
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
var escStr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
472
|
+
var cssIdSelector = (id) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id) ? `#${id}` : `[id="${id.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"]`;
|
|
473
|
+
var ANCHOR_KIND_SCORES = { testid: 80, id: 74, labeledRole: 68, role: 62 };
|
|
474
|
+
function anchorSegment(a) {
|
|
475
|
+
if (a.testId && a.testIdCount === 1) {
|
|
476
|
+
return { code: `getByTestId('${escStr(a.testId)}')`, kind: "testid", flatArgs: { anchorTestId: a.testId } };
|
|
477
|
+
}
|
|
478
|
+
if (a.id && !isAutoGenerated(a.id) && a.idCount === 1) {
|
|
479
|
+
const anchorSelector = cssIdSelector(a.id);
|
|
480
|
+
return { code: `locator('${escStr(anchorSelector)}')`, kind: "id", flatArgs: { anchorSelector } };
|
|
481
|
+
}
|
|
482
|
+
if (a.role && a.ariaLabel && a.labeledRoleCount === 1) {
|
|
483
|
+
return {
|
|
484
|
+
code: `getByRole('${escStr(a.role)}', { name: '${escStr(a.ariaLabel)}' })`,
|
|
485
|
+
kind: "labeledRole",
|
|
486
|
+
flatArgs: { anchorRole: a.role, anchorName: a.ariaLabel }
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (a.role && a.roleCount === 1) {
|
|
490
|
+
return { code: `getByRole('${escStr(a.role)}')`, kind: "role", flatArgs: { anchorRole: a.role } };
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
495
|
+
if (!leaf.role || anchors.length === 0) return [];
|
|
496
|
+
const out = [];
|
|
497
|
+
const seen = /* @__PURE__ */ new Set();
|
|
498
|
+
const rolePart = leaf.level != null ? `'${leaf.role}', { level: ${leaf.level} }` : `'${leaf.role}'`;
|
|
499
|
+
const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
|
|
500
|
+
const add = (l) => {
|
|
501
|
+
if (!seen.has(l.locator)) {
|
|
502
|
+
seen.add(l.locator);
|
|
503
|
+
out.push(l);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
for (const anchor of anchors) {
|
|
507
|
+
if (anchor.scopedLeafCount !== 1) continue;
|
|
508
|
+
const seg = anchorSegment(anchor);
|
|
509
|
+
if (!seg) continue;
|
|
510
|
+
add({
|
|
511
|
+
locator: `${seg.code}.getByRole(${rolePart})`,
|
|
512
|
+
method: "getByRole",
|
|
513
|
+
args: { ...leafArgs, ...seg.flatArgs },
|
|
514
|
+
score: ANCHOR_KIND_SCORES[seg.kind]
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (anchors.length >= 2 && chainLeafCount === 1) {
|
|
518
|
+
const ordered = [...anchors].sort((a, b) => b.depth - a.depth);
|
|
519
|
+
const segments = ordered.map(anchorSegment);
|
|
520
|
+
if (segments.every((s) => s !== null)) {
|
|
521
|
+
const innermost = segments[segments.length - 1];
|
|
522
|
+
const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
|
|
523
|
+
add({
|
|
524
|
+
locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
|
|
525
|
+
method: "getByRole",
|
|
526
|
+
// Flat args describe the innermost anchor (what the healing fingerprint
|
|
527
|
+
// logic understands); the full chain rides along for transparency.
|
|
528
|
+
args: { ...leafArgs, ...innermost.flatArgs, anchorChain: segments.map((s) => s.code) },
|
|
529
|
+
score
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return out.sort((a, b) => b.score - a.score);
|
|
534
|
+
}
|
|
535
|
+
function mergeCandidates(base, extra) {
|
|
536
|
+
const seen = new Set(base.map((b) => b.locator));
|
|
537
|
+
const merged = [...base];
|
|
538
|
+
for (const e of extra) {
|
|
539
|
+
if (seen.has(e.locator)) continue;
|
|
540
|
+
seen.add(e.locator);
|
|
541
|
+
merged.push(e);
|
|
542
|
+
}
|
|
543
|
+
return merged.sort((a, b) => b.score - a.score);
|
|
544
|
+
}
|
|
545
|
+
function installPickerOverlay(arg) {
|
|
546
|
+
const g = globalThis;
|
|
547
|
+
const doc = g.document;
|
|
548
|
+
if (!doc || !doc.body) {
|
|
549
|
+
g.__piwiPickState = "skipped";
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const Z = 2147483600;
|
|
553
|
+
const highlight = doc.createElement("div");
|
|
554
|
+
highlight.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};display:none;box-sizing:border-box;border:2px solid #7c3aed;background:rgba(124,58,237,.12);border-radius:3px;`;
|
|
555
|
+
const banner = doc.createElement("div");
|
|
556
|
+
banner.style.cssText = `position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:${Z + 2};background:#111827;color:#f9fafb;font:13px/1.5 system-ui,sans-serif;padding:10px 16px;border-radius:8px;box-shadow:0 4px 24px rgba(0,0,0,.4);max-width:80vw;`;
|
|
557
|
+
const hlLocator = (expr) => {
|
|
558
|
+
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
559
|
+
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
560
|
+
let html = "";
|
|
561
|
+
let last = 0;
|
|
562
|
+
let m;
|
|
563
|
+
while ((m = re.exec(expr)) !== null) {
|
|
564
|
+
if (m.index > last) html += escHtml(expr.slice(last, m.index));
|
|
565
|
+
const color = m[1] ? "#4ade80" : m[2] ? "#c084fc" : m[3] ? "#93c5fd" : m[4] ? "#fbbf24" : "#9ca3af";
|
|
566
|
+
html += `<span style="color:${color}">${escHtml(m[0])}</span>`;
|
|
567
|
+
last = re.lastIndex;
|
|
568
|
+
}
|
|
569
|
+
if (last < expr.length) html += escHtml(expr.slice(last));
|
|
570
|
+
return `<code style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace">${html}</code>`;
|
|
571
|
+
};
|
|
572
|
+
const head = doc.createElement("div");
|
|
573
|
+
head.innerHTML = arg.failing ? `Piwi locator picker \u2014 click the element that should replace ${hlLocator(arg.failing)}` : "Piwi inspector \u2014 click any element to generate locators for it";
|
|
574
|
+
const foot = doc.createElement("div");
|
|
575
|
+
foot.style.cssText = "color:#9ca3af;margin-top:3px;";
|
|
576
|
+
foot.textContent = "\u2191 parent \xB7 \u2193 child \xB7 Esc skip";
|
|
577
|
+
banner.appendChild(head);
|
|
578
|
+
banner.appendChild(foot);
|
|
579
|
+
doc.body.appendChild(highlight);
|
|
580
|
+
doc.body.appendChild(banner);
|
|
581
|
+
const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
582
|
+
const describe = (el) => {
|
|
583
|
+
const tag = (el.tagName || "?").toLowerCase();
|
|
584
|
+
const testId = el.getAttribute && el.getAttribute("data-testid");
|
|
585
|
+
if (testId) return `getByTestId('${escJs(testId)}')`;
|
|
586
|
+
if (el.labels && el.labels.length > 0) {
|
|
587
|
+
const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
588
|
+
if (labelText) return `getByLabel('${escJs(labelText)}')`;
|
|
589
|
+
}
|
|
590
|
+
const ariaLabel = el.getAttribute && el.getAttribute("aria-label");
|
|
591
|
+
if (ariaLabel) return `getByLabel('${escJs(ariaLabel)}')`;
|
|
592
|
+
const placeholder = el.getAttribute && el.getAttribute("placeholder");
|
|
593
|
+
if (placeholder) return `getByPlaceholder('${escJs(placeholder)}')`;
|
|
594
|
+
const alt = el.getAttribute && el.getAttribute("alt");
|
|
595
|
+
if (alt) return `getByAltText('${escJs(alt)}')`;
|
|
596
|
+
const titleAttr = el.getAttribute && el.getAttribute("title");
|
|
597
|
+
if (titleAttr) return `getByTitle('${escJs(titleAttr)}')`;
|
|
598
|
+
if (el.id) return `locator('#${escJs(el.id)}')`;
|
|
599
|
+
const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
|
|
600
|
+
return cls ? `locator('.${escJs(cls)}')` : tag;
|
|
601
|
+
};
|
|
602
|
+
const buildChain = (raw) => {
|
|
603
|
+
const chain2 = [];
|
|
604
|
+
let node = raw;
|
|
605
|
+
while (node && chain2.length < 15) {
|
|
606
|
+
const tag = (node.tagName || "").toLowerCase();
|
|
607
|
+
if (tag === "body" || tag === "html") break;
|
|
608
|
+
chain2.push(node);
|
|
609
|
+
node = node.parentElement;
|
|
610
|
+
}
|
|
611
|
+
return chain2.length ? chain2 : [raw];
|
|
612
|
+
};
|
|
613
|
+
const ACTIONABLE_TAGS = ["button", "a", "input", "select", "textarea", "summary", "option"];
|
|
614
|
+
const snapIndex = (chain2) => {
|
|
615
|
+
for (let i = 0; i < Math.min(chain2.length, 4); i++) {
|
|
616
|
+
const el = chain2[i];
|
|
617
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
618
|
+
if (ACTIONABLE_TAGS.includes(tag)) return i;
|
|
619
|
+
if (el.getAttribute && (el.getAttribute("role") || el.getAttribute("data-testid"))) return i;
|
|
620
|
+
}
|
|
621
|
+
return 0;
|
|
622
|
+
};
|
|
623
|
+
let chain = [];
|
|
624
|
+
let idx = 0;
|
|
625
|
+
let lastRaw = null;
|
|
626
|
+
const current = () => chain[idx] ?? null;
|
|
627
|
+
const refresh = () => {
|
|
628
|
+
const el = current();
|
|
629
|
+
if (!el) {
|
|
630
|
+
highlight.style.display = "none";
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const r = el.getBoundingClientRect();
|
|
634
|
+
highlight.style.display = "block";
|
|
635
|
+
highlight.style.left = r.left + "px";
|
|
636
|
+
highlight.style.top = r.top + "px";
|
|
637
|
+
highlight.style.width = r.width + "px";
|
|
638
|
+
highlight.style.height = r.height + "px";
|
|
639
|
+
foot.textContent = `${describe(el)} \u2014 click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip`;
|
|
640
|
+
};
|
|
641
|
+
const stop = (e) => {
|
|
642
|
+
e.preventDefault();
|
|
643
|
+
e.stopImmediatePropagation();
|
|
644
|
+
};
|
|
645
|
+
const isOwn = (el) => el === banner || el === highlight || banner.contains && banner.contains(el);
|
|
646
|
+
let bannerDocked = "top";
|
|
647
|
+
const dockBanner = (side) => {
|
|
648
|
+
if (bannerDocked === side) return;
|
|
649
|
+
bannerDocked = side;
|
|
650
|
+
if (side === "bottom") {
|
|
651
|
+
banner.style.top = "auto";
|
|
652
|
+
banner.style.bottom = "12px";
|
|
653
|
+
} else {
|
|
654
|
+
banner.style.top = "12px";
|
|
655
|
+
banner.style.bottom = "auto";
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
const onMove = (e) => {
|
|
659
|
+
const raw = e.target;
|
|
660
|
+
if (!raw || isOwn(raw)) {
|
|
661
|
+
highlight.style.display = "none";
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (raw !== lastRaw) {
|
|
665
|
+
lastRaw = raw;
|
|
666
|
+
chain = buildChain(raw);
|
|
667
|
+
idx = snapIndex(chain);
|
|
668
|
+
}
|
|
669
|
+
refresh();
|
|
670
|
+
const el = current();
|
|
671
|
+
if (el) {
|
|
672
|
+
const r = el.getBoundingClientRect();
|
|
673
|
+
const br = banner.getBoundingClientRect();
|
|
674
|
+
const margin = 8;
|
|
675
|
+
if (r.left < br.right + margin && r.right > br.left - margin && r.top < br.bottom + margin && r.bottom > br.top - margin) {
|
|
676
|
+
dockBanner(bannerDocked === "top" ? "bottom" : "top");
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
const onClick = (e) => {
|
|
681
|
+
stop(e);
|
|
682
|
+
const el = current();
|
|
683
|
+
if (!el || isOwn(e.target)) return;
|
|
684
|
+
g.__piwiPickedElement = el;
|
|
685
|
+
g.__piwiPickState = "picked";
|
|
686
|
+
removeListeners();
|
|
687
|
+
highlight.style.display = "none";
|
|
688
|
+
foot.textContent = "Analyzing element\u2026";
|
|
689
|
+
};
|
|
690
|
+
const onKey = (e) => {
|
|
691
|
+
if (e.key === "Escape") {
|
|
692
|
+
stop(e);
|
|
693
|
+
g.__piwiPickState = "skipped";
|
|
694
|
+
cleanup();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
698
|
+
stop(e);
|
|
699
|
+
if (e.key === "ArrowUp") idx = Math.min(idx + 1, chain.length - 1);
|
|
700
|
+
else idx = Math.max(idx - 1, 0);
|
|
701
|
+
refresh();
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
const suppressed = ["mousedown", "mouseup", "pointerdown", "pointerup", "auxclick", "dblclick"];
|
|
705
|
+
const removeListeners = () => {
|
|
706
|
+
doc.removeEventListener("mousemove", onMove, true);
|
|
707
|
+
doc.removeEventListener("click", onClick, true);
|
|
708
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
709
|
+
for (const t of suppressed) doc.removeEventListener(t, stop, true);
|
|
710
|
+
};
|
|
711
|
+
const cleanup = () => {
|
|
712
|
+
removeListeners();
|
|
713
|
+
highlight.remove();
|
|
714
|
+
banner.remove();
|
|
715
|
+
};
|
|
716
|
+
g.__piwiPickCleanup = cleanup;
|
|
717
|
+
doc.addEventListener("mousemove", onMove, true);
|
|
718
|
+
doc.addEventListener("click", onClick, true);
|
|
719
|
+
doc.addEventListener("keydown", onKey, true);
|
|
720
|
+
for (const t of suppressed) doc.addEventListener(t, stop, true);
|
|
721
|
+
}
|
|
722
|
+
function showAnchorPicker(arg) {
|
|
723
|
+
const g = globalThis;
|
|
724
|
+
const doc = g.document;
|
|
725
|
+
const el = g.__piwiPickedElement;
|
|
726
|
+
if (!doc || !doc.body || !el) {
|
|
727
|
+
g.__piwiAnchorState = "skipped";
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const Z = 2147483600;
|
|
731
|
+
const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
|
|
732
|
+
const roleOf = (n) => {
|
|
733
|
+
const explicit = n.getAttribute && n.getAttribute("role");
|
|
734
|
+
if (explicit) return explicit;
|
|
735
|
+
const tag = (n.tagName || "").toLowerCase();
|
|
736
|
+
if (tag === "input") return inputRoles[(n.getAttribute("type") || "text").toLowerCase()] ?? "textbox";
|
|
737
|
+
if (tag === "select") return n.getAttribute("multiple") != null ? "listbox" : "combobox";
|
|
738
|
+
if (tag === "a") return n.getAttribute("href") != null ? "link" : null;
|
|
739
|
+
return tagRoles[tag] ?? null;
|
|
740
|
+
};
|
|
741
|
+
const levelOf = (n) => {
|
|
742
|
+
const m = /^h([1-6])$/.exec((n.tagName || "").toLowerCase());
|
|
743
|
+
if (m) return Number(m[1]);
|
|
744
|
+
const al = n.getAttribute && n.getAttribute("aria-level");
|
|
745
|
+
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
746
|
+
};
|
|
747
|
+
const leafMatches = (scope) => {
|
|
748
|
+
try {
|
|
749
|
+
if (leafTestId) return scope.querySelectorAll(`[data-testid=${JSON.stringify(leafTestId)}]`).length;
|
|
750
|
+
const nodes = scope.querySelectorAll(roleSources);
|
|
751
|
+
if (nodes.length > 2e3) return -1;
|
|
752
|
+
let matched = 0;
|
|
753
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
754
|
+
const n = nodes[i];
|
|
755
|
+
if (roleOf(n) !== leafRole) continue;
|
|
756
|
+
if (leafLevel != null && levelOf(n) !== leafLevel) continue;
|
|
757
|
+
matched++;
|
|
758
|
+
}
|
|
759
|
+
return matched;
|
|
760
|
+
} catch {
|
|
761
|
+
return -1;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
let roleNodes = [];
|
|
765
|
+
try {
|
|
766
|
+
const all = doc.querySelectorAll(roleSources);
|
|
767
|
+
if (all.length <= 4e3) roleNodes = Array.from(all);
|
|
768
|
+
} catch {
|
|
769
|
+
roleNodes = [];
|
|
770
|
+
}
|
|
771
|
+
const count = (sel) => {
|
|
772
|
+
try {
|
|
773
|
+
return doc.querySelectorAll(sel).length;
|
|
774
|
+
} catch {
|
|
775
|
+
return void 0;
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
const rows = [];
|
|
779
|
+
let node = el.parentElement;
|
|
780
|
+
let depth = 0;
|
|
781
|
+
while (node && depth < 12) {
|
|
782
|
+
depth++;
|
|
783
|
+
const tag = (node.tagName || "").toLowerCase();
|
|
784
|
+
if (tag === "body" || tag === "html") break;
|
|
785
|
+
const testId = node.getAttribute("data-testid");
|
|
786
|
+
const id = node.getAttribute("id");
|
|
787
|
+
const ariaLabel = node.getAttribute("aria-label");
|
|
788
|
+
const role = roleOf(node);
|
|
789
|
+
const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
|
|
790
|
+
if (testId) info.testIdCount = count(`[data-testid=${JSON.stringify(testId)}]`);
|
|
791
|
+
if (id) {
|
|
792
|
+
try {
|
|
793
|
+
info.idCount = count(`#${doc.defaultView.CSS.escape(id)}`);
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (role) {
|
|
798
|
+
let roleCount = 0;
|
|
799
|
+
let labeledCount = 0;
|
|
800
|
+
for (const n of roleNodes) {
|
|
801
|
+
if (roleOf(n) !== role) continue;
|
|
802
|
+
roleCount++;
|
|
803
|
+
if (ariaLabel && n.getAttribute && n.getAttribute("aria-label") === ariaLabel) labeledCount++;
|
|
804
|
+
}
|
|
805
|
+
info.roleCount = roleCount;
|
|
806
|
+
if (ariaLabel) info.labeledRoleCount = labeledCount;
|
|
807
|
+
}
|
|
808
|
+
info.scopedLeafCount = leafMatches(node);
|
|
809
|
+
const hookLabel = testId ? `data-testid="${testId}"` : id ? `#${id}` : ariaLabel && role ? `${role} "${ariaLabel}"` : role ? `role ${role}` : "no stable hook";
|
|
810
|
+
rows.push({
|
|
811
|
+
node,
|
|
812
|
+
info,
|
|
813
|
+
hookLabel,
|
|
814
|
+
selectable: !!(testId || id || role && (ariaLabel || info.roleCount === 1))
|
|
815
|
+
});
|
|
816
|
+
node = node.parentElement;
|
|
817
|
+
}
|
|
818
|
+
if (rows.length === 0) {
|
|
819
|
+
g.__piwiAnchorState = "skipped";
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const outline = doc.createElement("div");
|
|
823
|
+
outline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #22c55e;border-radius:3px;display:none;`;
|
|
824
|
+
const pickedOutline = doc.createElement("div");
|
|
825
|
+
const pr = el.getBoundingClientRect();
|
|
826
|
+
pickedOutline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #7c3aed;background:rgba(124,58,237,.10);border-radius:3px;left:${pr.left}px;top:${pr.top}px;width:${pr.width}px;height:${pr.height}px;`;
|
|
827
|
+
const panel = doc.createElement("div");
|
|
828
|
+
panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:340px;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);`;
|
|
829
|
+
const title = doc.createElement("div");
|
|
830
|
+
title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
|
|
831
|
+
title.textContent = "Scope to stable parents (optional)";
|
|
832
|
+
const sub = doc.createElement("div");
|
|
833
|
+
sub.style.cssText = "color:#9ca3af;margin-bottom:10px;";
|
|
834
|
+
sub.textContent = "Pick one or more parents to anchor the locator to. Hover a row to see the parent.";
|
|
835
|
+
panel.appendChild(title);
|
|
836
|
+
panel.appendChild(sub);
|
|
837
|
+
const selected = /* @__PURE__ */ new Set();
|
|
838
|
+
const footer = doc.createElement("div");
|
|
839
|
+
footer.style.cssText = "margin:10px 0;font-weight:600;";
|
|
840
|
+
const segMatches = (scope, info) => {
|
|
841
|
+
try {
|
|
842
|
+
if (info.testId) return Array.from(scope.querySelectorAll(`[data-testid=${JSON.stringify(info.testId)}]`));
|
|
843
|
+
if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
|
|
844
|
+
const nodes = Array.from(scope.querySelectorAll(roleSources));
|
|
845
|
+
if (nodes.length > 2e3) return [];
|
|
846
|
+
return nodes.filter(
|
|
847
|
+
(n) => roleOf(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
|
|
848
|
+
);
|
|
849
|
+
} catch {
|
|
850
|
+
return [];
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
const chainCount = () => {
|
|
854
|
+
const chosen = rows.filter((_, i) => selected.has(i)).sort((a, b) => b.info.depth - a.info.depth);
|
|
855
|
+
if (chosen.length === 0) return -1;
|
|
856
|
+
let scopes = [doc];
|
|
857
|
+
for (const row of chosen) {
|
|
858
|
+
const next = [];
|
|
859
|
+
for (const s of scopes) next.push(...segMatches(s, row.info));
|
|
860
|
+
scopes = next.slice(0, 200);
|
|
861
|
+
if (scopes.length === 0) return 0;
|
|
862
|
+
}
|
|
863
|
+
let total = 0;
|
|
864
|
+
for (const s of scopes) {
|
|
865
|
+
const c = leafMatches(s);
|
|
866
|
+
if (c > 0) total += c;
|
|
867
|
+
if (total > 50) return total;
|
|
868
|
+
}
|
|
869
|
+
return total;
|
|
870
|
+
};
|
|
871
|
+
const refreshFooter = () => {
|
|
872
|
+
if (selected.size === 0) {
|
|
873
|
+
footer.textContent = "No parents selected \u2014 standard alternatives only.";
|
|
874
|
+
footer.style.color = "#9ca3af";
|
|
875
|
+
g.__piwiPickChainCount = void 0;
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const c = chainCount();
|
|
879
|
+
g.__piwiPickChainCount = c;
|
|
880
|
+
if (c === 1) {
|
|
881
|
+
footer.textContent = "\u2713 Selection matches exactly 1 element";
|
|
882
|
+
footer.style.color = "#4ade80";
|
|
883
|
+
} else {
|
|
884
|
+
footer.textContent = c < 0 ? "Match count unavailable" : `\u2717 Selection matches ${c} elements`;
|
|
885
|
+
footer.style.color = "#fbbf24";
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
rows.forEach((row, i) => {
|
|
889
|
+
const line = doc.createElement("label");
|
|
890
|
+
line.style.cssText = `display:flex;align-items:center;gap:8px;padding:6px 8px;border:1px solid #374151;border-radius:6px;margin-bottom:6px;cursor:${row.selectable ? "pointer" : "default"};opacity:${row.selectable ? "1" : ".45"};`;
|
|
891
|
+
const box = doc.createElement("input");
|
|
892
|
+
box.type = "checkbox";
|
|
893
|
+
box.disabled = !row.selectable;
|
|
894
|
+
const text = doc.createElement("span");
|
|
895
|
+
text.style.cssText = "flex:1;min-width:0;";
|
|
896
|
+
const code = doc.createElement("code");
|
|
897
|
+
code.style.cssText = "display:block;font:11px ui-monospace,monospace;color:#e5e7eb;word-break:break-all;";
|
|
898
|
+
code.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
899
|
+
const hint = doc.createElement("span");
|
|
900
|
+
hint.style.cssText = "color:#9ca3af;";
|
|
901
|
+
hint.textContent = row.selectable ? row.info.scopedLeafCount === 1 ? "contains exactly 1 matching element" : `contains ${row.info.scopedLeafCount ?? "?"} matching elements` : "add a data-testid to make this usable";
|
|
902
|
+
text.appendChild(code);
|
|
903
|
+
text.appendChild(hint);
|
|
904
|
+
line.appendChild(box);
|
|
905
|
+
line.appendChild(text);
|
|
906
|
+
line.addEventListener("mouseenter", () => {
|
|
907
|
+
const r = row.node.getBoundingClientRect();
|
|
908
|
+
outline.style.display = "block";
|
|
909
|
+
outline.style.left = r.left + "px";
|
|
910
|
+
outline.style.top = r.top + "px";
|
|
911
|
+
outline.style.width = r.width + "px";
|
|
912
|
+
outline.style.height = r.height + "px";
|
|
913
|
+
});
|
|
914
|
+
line.addEventListener("mouseleave", () => {
|
|
915
|
+
outline.style.display = "none";
|
|
916
|
+
});
|
|
917
|
+
box.addEventListener("change", () => {
|
|
918
|
+
if (box.checked) selected.add(i);
|
|
919
|
+
else selected.delete(i);
|
|
920
|
+
refreshFooter();
|
|
921
|
+
});
|
|
922
|
+
panel.appendChild(line);
|
|
923
|
+
});
|
|
924
|
+
panel.appendChild(footer);
|
|
925
|
+
const cleanup = () => {
|
|
926
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
927
|
+
panel.remove();
|
|
928
|
+
outline.remove();
|
|
929
|
+
pickedOutline.remove();
|
|
930
|
+
};
|
|
931
|
+
const done = (state) => {
|
|
932
|
+
g.__piwiPickAnchors = state === "done" ? rows.filter((_, i) => selected.has(i)).map((r) => r.info) : [];
|
|
933
|
+
g.__piwiAnchorState = state;
|
|
934
|
+
cleanup();
|
|
935
|
+
};
|
|
936
|
+
const onKey = (e) => {
|
|
937
|
+
if (e.key !== "Escape") return;
|
|
938
|
+
e.preventDefault();
|
|
939
|
+
e.stopImmediatePropagation();
|
|
940
|
+
done("skipped");
|
|
941
|
+
};
|
|
942
|
+
const buttonRow = doc.createElement("div");
|
|
943
|
+
buttonRow.style.cssText = "display:flex;gap:8px;margin-top:4px;";
|
|
944
|
+
const useBtn = doc.createElement("button");
|
|
945
|
+
useBtn.style.cssText = "flex:1;background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer;font:600 12px system-ui;";
|
|
946
|
+
useBtn.textContent = "Use selected parents";
|
|
947
|
+
useBtn.addEventListener("click", (e) => {
|
|
948
|
+
e.preventDefault();
|
|
949
|
+
e.stopImmediatePropagation();
|
|
950
|
+
done(selected.size > 0 ? "done" : "skipped");
|
|
951
|
+
});
|
|
952
|
+
const skipBtn = doc.createElement("button");
|
|
953
|
+
skipBtn.style.cssText = "background:none;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:8px 10px;cursor:pointer;font:12px system-ui;";
|
|
954
|
+
skipBtn.textContent = "Skip (Esc)";
|
|
955
|
+
skipBtn.addEventListener("click", (e) => {
|
|
956
|
+
e.preventDefault();
|
|
957
|
+
e.stopImmediatePropagation();
|
|
958
|
+
done("skipped");
|
|
959
|
+
});
|
|
960
|
+
buttonRow.appendChild(useBtn);
|
|
961
|
+
buttonRow.appendChild(skipBtn);
|
|
962
|
+
panel.appendChild(buttonRow);
|
|
963
|
+
refreshFooter();
|
|
964
|
+
g.__piwiAnchorCleanup = cleanup;
|
|
965
|
+
doc.addEventListener("keydown", onKey, true);
|
|
966
|
+
doc.body.appendChild(pickedOutline);
|
|
967
|
+
doc.body.appendChild(outline);
|
|
968
|
+
doc.body.appendChild(panel);
|
|
969
|
+
}
|
|
970
|
+
function showPickerChoices(arg) {
|
|
971
|
+
const g = globalThis;
|
|
972
|
+
const doc = g.document;
|
|
973
|
+
if (!doc || !doc.body) {
|
|
974
|
+
g.__piwiPickChoice = -1;
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const Z = 2147483600;
|
|
978
|
+
const wrap = doc.createElement("div");
|
|
979
|
+
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;`;
|
|
980
|
+
const panel = doc.createElement("div");
|
|
981
|
+
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);";
|
|
982
|
+
const hlLocator = (expr) => {
|
|
983
|
+
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
984
|
+
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
985
|
+
let html = "";
|
|
986
|
+
let last = 0;
|
|
987
|
+
let m;
|
|
988
|
+
while ((m = re.exec(expr)) !== null) {
|
|
989
|
+
if (m.index > last) html += escHtml(expr.slice(last, m.index));
|
|
990
|
+
const color = m[1] ? "#4ade80" : m[2] ? "#c084fc" : m[3] ? "#93c5fd" : m[4] ? "#fbbf24" : "#9ca3af";
|
|
991
|
+
html += `<span style="color:${color}">${escHtml(m[0])}</span>`;
|
|
992
|
+
last = re.lastIndex;
|
|
993
|
+
}
|
|
994
|
+
if (last < expr.length) html += escHtml(expr.slice(last));
|
|
995
|
+
return html;
|
|
996
|
+
};
|
|
997
|
+
const title = doc.createElement("div");
|
|
998
|
+
title.style.cssText = "font-weight:600;margin-bottom:4px;";
|
|
999
|
+
title.textContent = arg.failing ? "Pick a replacement locator" : "Pick a locator";
|
|
1000
|
+
const sub = doc.createElement("div");
|
|
1001
|
+
sub.style.cssText = "color:#9ca3af;margin-bottom:12px;";
|
|
1002
|
+
if (arg.failing) {
|
|
1003
|
+
sub.innerHTML = `Replaces <code style="font-family:ui-monospace,Menlo,monospace">${hlLocator(arg.failing)}</code> \u2014 ranked by stability score.`;
|
|
1004
|
+
} else {
|
|
1005
|
+
sub.textContent = "For the element you picked \u2014 ranked by stability score.";
|
|
1006
|
+
}
|
|
1007
|
+
panel.appendChild(title);
|
|
1008
|
+
panel.appendChild(sub);
|
|
1009
|
+
const done = (choice) => {
|
|
1010
|
+
g.__piwiPickChoice = choice;
|
|
1011
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
1012
|
+
wrap.remove();
|
|
1013
|
+
};
|
|
1014
|
+
const onKey = (e) => {
|
|
1015
|
+
if (e.key !== "Escape") return;
|
|
1016
|
+
e.preventDefault();
|
|
1017
|
+
e.stopImmediatePropagation();
|
|
1018
|
+
done(-1);
|
|
1019
|
+
};
|
|
1020
|
+
arg.choices.forEach((c, i) => {
|
|
1021
|
+
const btn = doc.createElement("button");
|
|
1022
|
+
btn.style.cssText = "display:flex;justify-content:space-between;align-items:center;gap:12px;width:100%;text-align:left;background:#1f2937;color:#e5e7eb;border:1px solid #374151;border-radius:6px;padding:8px 12px;margin:0 0 8px;cursor:pointer;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;";
|
|
1023
|
+
const code = doc.createElement("span");
|
|
1024
|
+
code.innerHTML = hlLocator(c.locator);
|
|
1025
|
+
code.style.cssText = "word-break:break-all;";
|
|
1026
|
+
const score = doc.createElement("span");
|
|
1027
|
+
score.textContent = String(c.score);
|
|
1028
|
+
score.style.cssText = "color:#a78bfa;flex-shrink:0;";
|
|
1029
|
+
btn.appendChild(code);
|
|
1030
|
+
btn.appendChild(score);
|
|
1031
|
+
btn.addEventListener("click", (e) => {
|
|
1032
|
+
e.preventDefault();
|
|
1033
|
+
e.stopImmediatePropagation();
|
|
1034
|
+
done(i);
|
|
1035
|
+
});
|
|
1036
|
+
panel.appendChild(btn);
|
|
1037
|
+
});
|
|
1038
|
+
const skip = doc.createElement("button");
|
|
1039
|
+
skip.style.cssText = "background:none;border:none;color:#9ca3af;cursor:pointer;padding:6px 0 0;font:12px system-ui,sans-serif;";
|
|
1040
|
+
skip.textContent = "Skip \u2014 keep the failure as-is (Esc)";
|
|
1041
|
+
skip.addEventListener("click", (e) => {
|
|
1042
|
+
e.preventDefault();
|
|
1043
|
+
e.stopImmediatePropagation();
|
|
1044
|
+
done(-1);
|
|
1045
|
+
});
|
|
1046
|
+
panel.appendChild(skip);
|
|
1047
|
+
doc.addEventListener("keydown", onKey, true);
|
|
1048
|
+
wrap.appendChild(panel);
|
|
1049
|
+
doc.body.appendChild(wrap);
|
|
1050
|
+
}
|
|
1051
|
+
async function cleanupPicker(page) {
|
|
1052
|
+
try {
|
|
1053
|
+
await page.evaluate(() => {
|
|
1054
|
+
const g = globalThis;
|
|
1055
|
+
if (typeof g.__piwiPickCleanup === "function") g.__piwiPickCleanup();
|
|
1056
|
+
if (typeof g.__piwiAnchorCleanup === "function") g.__piwiAnchorCleanup();
|
|
1057
|
+
delete g.__piwiPickCleanup;
|
|
1058
|
+
delete g.__piwiAnchorCleanup;
|
|
1059
|
+
delete g.__piwiPickState;
|
|
1060
|
+
delete g.__piwiPickChoice;
|
|
1061
|
+
delete g.__piwiPickedElement;
|
|
1062
|
+
delete g.__piwiAnchorState;
|
|
1063
|
+
delete g.__piwiPickAnchors;
|
|
1064
|
+
delete g.__piwiPickChainCount;
|
|
1065
|
+
});
|
|
1066
|
+
} catch {
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
1070
|
+
try {
|
|
1071
|
+
testInfo.setTimeout(0);
|
|
1072
|
+
const rendered = failed ? renderFailing(failed) : null;
|
|
1073
|
+
console.log(
|
|
1074
|
+
`
|
|
1075
|
+
[piwi] "${testInfo.title}" ${testInfo.status} \u2014 ${rendered ? "locator picker" : "inspector"} open in the browser: ${rendered ? `click the element that should replace ${rendered}` : "click any element to generate locators for it"} (\u2191/\u2193 to select a parent/child, Esc to skip).`
|
|
1076
|
+
);
|
|
1077
|
+
await page.evaluate(installPickerOverlay, { failing: rendered });
|
|
1078
|
+
await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
|
|
1079
|
+
timeout: 0,
|
|
1080
|
+
polling: 250
|
|
1081
|
+
});
|
|
1082
|
+
const state = await page.evaluate(() => globalThis.__piwiPickState);
|
|
1083
|
+
if (state !== "picked") {
|
|
1084
|
+
await cleanupPicker(page);
|
|
1085
|
+
return null;
|
|
1086
|
+
}
|
|
1087
|
+
const handle = await page.evaluateHandle(() => globalThis.__piwiPickedElement);
|
|
1088
|
+
const attrs = await handle.evaluate(probe.fn, probe.arg);
|
|
1089
|
+
await handle.dispose();
|
|
1090
|
+
const accessibleName = approximateAccessibleName({ ...attrs, accessibleName: null });
|
|
1091
|
+
const role = resolveAriaRole({ ...attrs, accessibleName });
|
|
1092
|
+
const level = headingLevel({ ...attrs, accessibleName }, role);
|
|
1093
|
+
let anchors = [];
|
|
1094
|
+
let chainLeafCount;
|
|
1095
|
+
if (role) {
|
|
1096
|
+
const probeArg = probe.arg;
|
|
1097
|
+
await page.evaluate(showAnchorPicker, {
|
|
1098
|
+
tagRoles: probeArg.tagRoles,
|
|
1099
|
+
inputRoles: probeArg.inputRoles,
|
|
1100
|
+
roleSources: probeArg.roleSources,
|
|
1101
|
+
leafRole: role,
|
|
1102
|
+
leafLevel: level,
|
|
1103
|
+
leafTestId: attrs.attributes["data-testid"] ?? null
|
|
1104
|
+
});
|
|
1105
|
+
await page.waitForFunction(() => globalThis.__piwiAnchorState !== void 0, void 0, {
|
|
1106
|
+
timeout: 0,
|
|
1107
|
+
polling: 250
|
|
1108
|
+
});
|
|
1109
|
+
const anchorResult = await page.evaluate(() => {
|
|
1110
|
+
const g = globalThis;
|
|
1111
|
+
return { state: g.__piwiAnchorState, anchors: g.__piwiPickAnchors ?? [], chainCount: g.__piwiPickChainCount };
|
|
1112
|
+
});
|
|
1113
|
+
if (anchorResult.state === "done" && anchorResult.anchors.length > 0) {
|
|
1114
|
+
anchors = anchorResult.anchors;
|
|
1115
|
+
chainLeafCount = anchorResult.chainCount;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const ranked = mergeCandidates(
|
|
1119
|
+
generateAlternatives({ ...attrs, accessibleName }),
|
|
1120
|
+
generateAnchoredAlternatives({ role, level }, anchors, chainLeafCount)
|
|
1121
|
+
);
|
|
1122
|
+
if (ranked.length === 0) {
|
|
1123
|
+
console.log("[piwi] No stable locator could be generated for the picked element \u2014 nothing recorded.");
|
|
1124
|
+
await cleanupPicker(page);
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
await page.evaluate(showPickerChoices, {
|
|
1128
|
+
failing: rendered,
|
|
1129
|
+
choices: ranked.map((r) => ({ locator: r.locator, score: r.score }))
|
|
1130
|
+
});
|
|
1131
|
+
await page.waitForFunction(() => globalThis.__piwiPickChoice !== void 0, void 0, {
|
|
1132
|
+
timeout: 0,
|
|
1133
|
+
polling: 250
|
|
1134
|
+
});
|
|
1135
|
+
const choice = await page.evaluate(() => globalThis.__piwiPickChoice);
|
|
1136
|
+
await cleanupPicker(page);
|
|
1137
|
+
if (typeof choice !== "number" || choice < 0 || choice >= ranked.length) return null;
|
|
1138
|
+
const picked = { ...ranked[choice], pickedByUser: true };
|
|
1139
|
+
const alternatives = [picked, ...ranked.filter((_, i) => i !== choice)];
|
|
1140
|
+
const element = {
|
|
1141
|
+
tagName: attrs.tagName,
|
|
1142
|
+
attributes: attrs.attributes,
|
|
1143
|
+
textContent: attrs.textContent,
|
|
1144
|
+
accessibleName,
|
|
1145
|
+
center: attrs.center,
|
|
1146
|
+
...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
|
|
1147
|
+
...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
|
|
1148
|
+
};
|
|
1149
|
+
const location = failed?.location ?? null;
|
|
1150
|
+
console.log(
|
|
1151
|
+
failed ? `[piwi] Replacement picked for ${rendered}${location ? ` at ${location}` : ""}:
|
|
1152
|
+
[piwi] ${picked.locator}` : `[piwi] Locator picked while inspecting:
|
|
1153
|
+
[piwi] ${picked.locator}`
|
|
1154
|
+
);
|
|
1155
|
+
return {
|
|
1156
|
+
failing: failed ? { method: failed.method, args: failed.args, rendered, location } : null,
|
|
1157
|
+
picked,
|
|
1158
|
+
alternatives,
|
|
1159
|
+
element,
|
|
1160
|
+
...anchors.length > 0 ? { anchors } : {}
|
|
1161
|
+
};
|
|
1162
|
+
} catch {
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function applyPickToSnapshots(snapshots, pick) {
|
|
1167
|
+
if (!pick.failing?.location) return false;
|
|
1168
|
+
const failing = pick.failing;
|
|
1169
|
+
for (let i = snapshots.length - 1; i >= 0; i--) {
|
|
1170
|
+
const snap = snapshots[i];
|
|
1171
|
+
if (snap.location !== failing.location || snap.element) continue;
|
|
1172
|
+
snapshots[i] = {
|
|
1173
|
+
location: snap.location,
|
|
1174
|
+
used: snap.used,
|
|
1175
|
+
element: pick.element,
|
|
1176
|
+
alternatives: pick.alternatives.slice(0, 10)
|
|
1177
|
+
};
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
snapshots.push({
|
|
1181
|
+
location: failing.location,
|
|
1182
|
+
used: {
|
|
1183
|
+
method: failing.method,
|
|
1184
|
+
args: failing.args,
|
|
1185
|
+
raw: `${failing.method}(${JSON.stringify(failing.args)})`
|
|
1186
|
+
},
|
|
1187
|
+
element: pick.element,
|
|
1188
|
+
alternatives: pick.alternatives.slice(0, 10)
|
|
1189
|
+
});
|
|
1190
|
+
return true;
|
|
1191
|
+
}
|
|
1192
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1193
|
+
0 && (module.exports = {
|
|
1194
|
+
applyPickToSnapshots,
|
|
1195
|
+
deriveFailedLocator,
|
|
1196
|
+
generateAnchoredAlternatives,
|
|
1197
|
+
installPickerOverlay,
|
|
1198
|
+
mergeCandidates,
|
|
1199
|
+
parseLeafLocatorExpression,
|
|
1200
|
+
runLocatorPicker,
|
|
1201
|
+
showAnchorPicker,
|
|
1202
|
+
showPickerChoices
|
|
1203
|
+
});
|