@piwitests/reporter 0.20.0 → 0.22.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 +12 -0
- package/dist/cli/index.js +653 -10
- package/dist/global-setup-module.js +26 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1738 -1403
- package/dist/internal/capture/capture-fixtures.d.ts +3 -56
- package/dist/internal/capture/capture-fixtures.js +1658 -1349
- package/dist/internal/capture/locator-healing.js +75 -27
- package/dist/internal/capture/pick-on-failure.d.ts +6 -125
- package/dist/internal/capture/pick-on-failure.js +1108 -940
- package/package.json +4 -2
- package/templates/skills/apply-locator-healing/SKILL.md +32 -0
- package/templates/skills/investigate-failure/SKILL.md +36 -0
- package/templates/skills/setup-piwi/SKILL.md +62 -0
- package/templates/skills/stabilize-flaky-tests/SKILL.md +36 -0
|
@@ -41,758 +41,1267 @@ __export(capture_fixtures_exports, {
|
|
|
41
41
|
module.exports = __toCommonJS(capture_fixtures_exports);
|
|
42
42
|
var import_node_zlib = require("zlib");
|
|
43
43
|
|
|
44
|
-
// src/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!ariaSnapshot) return [];
|
|
52
|
-
const out = [];
|
|
53
|
-
for (const line of ariaSnapshot.split("\n")) {
|
|
54
|
-
const m = line.match(/^\s*-\s+([a-z]+)(?:\s+"((?:[^"\\]|\\.)*)")?/i);
|
|
55
|
-
if (!m) continue;
|
|
56
|
-
const role = m[1];
|
|
57
|
-
const name = m[2] != null ? m[2].replace(/\\(.)/g, "$1") : null;
|
|
58
|
-
if (!name && (role === "generic" || role === "group" || role === "list" || role === "paragraph")) continue;
|
|
59
|
-
const levelMatch = line.slice(m[0].length).match(/\[level=(\d+)\]/);
|
|
60
|
-
const level = levelMatch ? Number(levelMatch[1]) : null;
|
|
61
|
-
out.push({ role, name, level });
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
function textSimilarity(a, b) {
|
|
66
|
-
const tok = (s) => new Set(
|
|
67
|
-
(s ?? "").toLowerCase().split(/[^a-z0-9]+/i).filter(Boolean)
|
|
68
|
-
);
|
|
69
|
-
const sa = tok(a);
|
|
70
|
-
const sb = tok(b);
|
|
71
|
-
if (sa.size === 0 && sb.size === 0) return 1;
|
|
72
|
-
if (sa.size === 0 || sb.size === 0) return 0;
|
|
73
|
-
let common = 0;
|
|
74
|
-
for (const t of sa) if (sb.has(t)) common++;
|
|
75
|
-
return 2 * common / (sa.size + sb.size);
|
|
76
|
-
}
|
|
77
|
-
function fingerprintPresent(fp, candidates) {
|
|
78
|
-
if (!fp.name) return false;
|
|
79
|
-
return candidates.some(
|
|
80
|
-
(c) => (!fp.role || c.role === fp.role) && textSimilarity(c.name, fp.name) >= PRESENT_SIMILARITY
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
function matchRenamedElement(fp, candidates) {
|
|
84
|
-
if (candidates.length === 0) return null;
|
|
85
|
-
const sameRole = fp.role ? candidates.filter((c) => c.role === fp.role) : candidates;
|
|
86
|
-
if (sameRole.length === 0) return null;
|
|
87
|
-
let pool = sameRole;
|
|
88
|
-
if (fp.level != null) {
|
|
89
|
-
const sameLevel = sameRole.filter((c) => c.level === fp.level);
|
|
90
|
-
if (sameLevel.length > 0) pool = sameLevel;
|
|
91
|
-
}
|
|
92
|
-
if (pool.length === 1) {
|
|
93
|
-
return { candidate: pool[0], confidence: 0.7 };
|
|
44
|
+
// ../picker-dom/src/probe.ts
|
|
45
|
+
function probeElementAttrs(el, arg) {
|
|
46
|
+
const { keep, tagRoles, inputRoles, roleSources, includeStructural, includeLabelText } = arg;
|
|
47
|
+
const attrMap = {};
|
|
48
|
+
for (const key of keep) {
|
|
49
|
+
const v = el.getAttribute(key) ?? el[key];
|
|
50
|
+
attrMap[key] = typeof v === "string" ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
|
|
94
51
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
52
|
+
const r = el.getBoundingClientRect();
|
|
53
|
+
const selectorCounts = {};
|
|
54
|
+
try {
|
|
55
|
+
const doc = el.ownerDocument;
|
|
56
|
+
const cssEsc = (s) => doc.defaultView.CSS.escape(s);
|
|
57
|
+
const count = (sel) => {
|
|
58
|
+
try {
|
|
59
|
+
return doc.querySelectorAll(sel).length;
|
|
60
|
+
} catch {
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
if (attrMap["data-testid"]) {
|
|
65
|
+
selectorCounts.testId = count(`[data-testid=${JSON.stringify(attrMap["data-testid"])}]`);
|
|
66
|
+
}
|
|
67
|
+
if (attrMap["id"]) selectorCounts.id = count(`#${cssEsc(attrMap["id"])}`);
|
|
68
|
+
if (attrMap["name"]) selectorCounts.name = count(`[name=${JSON.stringify(attrMap["name"])}]`);
|
|
69
|
+
if (attrMap["placeholder"]) {
|
|
70
|
+
selectorCounts.placeholder = count(`[placeholder=${JSON.stringify(attrMap["placeholder"])}]`);
|
|
71
|
+
}
|
|
72
|
+
if (attrMap["alt"]) selectorCounts.alt = count(`[alt=${JSON.stringify(attrMap["alt"])}]`);
|
|
73
|
+
if (attrMap["title"]) selectorCounts.title = count(`[title=${JSON.stringify(attrMap["title"])}]`);
|
|
74
|
+
const classList = (attrMap["class"] || "").split(/\s+/).filter((c) => c.length > 1).slice(0, 10);
|
|
75
|
+
if (classList.length > 0) {
|
|
76
|
+
const classCounts = {};
|
|
77
|
+
for (const cls of classList) {
|
|
78
|
+
const n = count(`.${cssEsc(cls)}`);
|
|
79
|
+
if (n !== void 0) classCounts[cls] = n;
|
|
80
|
+
}
|
|
81
|
+
selectorCounts.classes = classCounts;
|
|
102
82
|
}
|
|
83
|
+
} catch {
|
|
103
84
|
}
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
85
|
+
let rolePosition = null;
|
|
86
|
+
const ancestors = [];
|
|
87
|
+
if (includeStructural && tagRoles && inputRoles && roleSources) {
|
|
88
|
+
try {
|
|
89
|
+
const doc = el.ownerDocument;
|
|
90
|
+
const cssEsc = (s) => doc.defaultView.CSS.escape(s);
|
|
91
|
+
const count = (sel) => {
|
|
92
|
+
try {
|
|
93
|
+
return doc.querySelectorAll(sel).length;
|
|
94
|
+
} catch {
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const roleMemo = /* @__PURE__ */ new Map();
|
|
99
|
+
const textMemo = /* @__PURE__ */ new Map();
|
|
100
|
+
const roleOf = (n) => {
|
|
101
|
+
const cached = roleMemo.get(n);
|
|
102
|
+
if (cached !== void 0) return cached;
|
|
103
|
+
let role;
|
|
104
|
+
const explicit = n.getAttribute("role");
|
|
105
|
+
if (explicit) {
|
|
106
|
+
role = explicit;
|
|
107
|
+
} else {
|
|
108
|
+
const tag = (n.tagName || "").toLowerCase();
|
|
109
|
+
if (tag === "input") role = inputRoles[(n.getAttribute("type") || "text").toLowerCase()] ?? "textbox";
|
|
110
|
+
else if (tag === "select") role = n.getAttribute("multiple") != null ? "listbox" : "combobox";
|
|
111
|
+
else if (tag === "a") role = n.getAttribute("href") != null ? "link" : null;
|
|
112
|
+
else role = tagRoles[tag] ?? null;
|
|
113
|
+
}
|
|
114
|
+
roleMemo.set(n, role);
|
|
115
|
+
return role;
|
|
116
|
+
};
|
|
117
|
+
const levelOf = (n) => {
|
|
118
|
+
const m = /^h([1-6])$/.exec((n.tagName || "").toLowerCase());
|
|
119
|
+
if (m) return Number(m[1]);
|
|
120
|
+
const al = n.getAttribute("aria-level");
|
|
121
|
+
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
122
|
+
};
|
|
123
|
+
const targetRole = roleOf(el);
|
|
124
|
+
const targetLevel = targetRole === "heading" ? levelOf(el) : null;
|
|
125
|
+
const nameOf = (n) => {
|
|
126
|
+
const al = n.getAttribute("aria-label");
|
|
127
|
+
if (al) return al;
|
|
128
|
+
const txt = (n.textContent || "").replace(/\s+/g, " ").trim();
|
|
129
|
+
if (txt) return txt;
|
|
130
|
+
return n.getAttribute("title") || n.getAttribute("placeholder") || null;
|
|
131
|
+
};
|
|
132
|
+
const targetName = nameOf(el);
|
|
133
|
+
const targetText = (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
134
|
+
const textNeedle = targetText ? targetText.toLowerCase() : null;
|
|
135
|
+
const normText = (n) => {
|
|
136
|
+
const cached = textMemo.get(n);
|
|
137
|
+
if (cached !== void 0) return cached;
|
|
138
|
+
const text = (n.textContent || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
139
|
+
textMemo.set(n, text);
|
|
140
|
+
return text;
|
|
141
|
+
};
|
|
142
|
+
const TEXT_COUNT_CAP = 2;
|
|
143
|
+
const countTextOwners = (root, cap) => {
|
|
144
|
+
if (!textNeedle) return -1;
|
|
145
|
+
const els = root.querySelectorAll("*");
|
|
146
|
+
if (els.length > cap) return -1;
|
|
147
|
+
let total = 0;
|
|
148
|
+
for (let i = 0; i < els.length; i++) {
|
|
149
|
+
const n = els[i];
|
|
150
|
+
if (normText(n).indexOf(textNeedle) === -1) continue;
|
|
151
|
+
let deeper = false;
|
|
152
|
+
const kids = n.children;
|
|
153
|
+
for (let j = 0; j < kids.length; j++) {
|
|
154
|
+
if (normText(kids[j]).indexOf(textNeedle) !== -1) {
|
|
155
|
+
deeper = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!deeper) total++;
|
|
160
|
+
if (total >= TEXT_COUNT_CAP) return total;
|
|
161
|
+
}
|
|
162
|
+
return total;
|
|
163
|
+
};
|
|
164
|
+
const nodes = doc.querySelectorAll(roleSources);
|
|
165
|
+
const nodesUsable = nodes.length <= 4e3;
|
|
166
|
+
const rolesUsable = !!targetRole && nodesUsable;
|
|
167
|
+
if (rolesUsable) {
|
|
168
|
+
let roleCountAll = 0;
|
|
169
|
+
let index = -1;
|
|
170
|
+
let levelCount = 0;
|
|
171
|
+
let roleNameCount = 0;
|
|
172
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
173
|
+
const n = nodes[i];
|
|
174
|
+
if (roleOf(n) !== targetRole) continue;
|
|
175
|
+
if (n === el) index = roleCountAll;
|
|
176
|
+
roleCountAll++;
|
|
177
|
+
if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
|
|
178
|
+
if (targetName != null && nameOf(n) === targetName) roleNameCount++;
|
|
179
|
+
}
|
|
180
|
+
if (targetName != null) selectorCounts.roleName = roleNameCount;
|
|
181
|
+
if (index !== -1) {
|
|
182
|
+
rolePosition = {
|
|
183
|
+
role: targetRole,
|
|
184
|
+
count: roleCountAll,
|
|
185
|
+
index,
|
|
186
|
+
...targetLevel != null ? { levelCount } : {}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (textNeedle) {
|
|
191
|
+
const textCount = countTextOwners(doc.body || doc.documentElement, 4e3);
|
|
192
|
+
if (textCount >= 0) selectorCounts.text = textCount;
|
|
193
|
+
}
|
|
194
|
+
if (rolesUsable || textNeedle) {
|
|
195
|
+
const CONTAINER_TAGS = ["form", "nav", "main", "article", "section", "dialog", "table", "li", "tr"];
|
|
196
|
+
const NOISY_DATA_ATTR = /^data-(v-[0-9a-f]+|reactid|react-checksum|svelte-\w+|ng-\w+|ember\w*)$/i;
|
|
197
|
+
const POSITIONAL_DATA_ATTR = /^data-(index|idx|i|key|row|rownum|col|column|position|pos|order|sort|offset|page)$/i;
|
|
198
|
+
const TEST_DATA_ATTRS2 = ["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"];
|
|
199
|
+
const usableDataAttr = (name, value) => {
|
|
200
|
+
if (!name || name.slice(0, 5) !== "data-" || name === "data-testid") return false;
|
|
201
|
+
if (NOISY_DATA_ATTR.test(name) || POSITIONAL_DATA_ATTR.test(name)) return false;
|
|
202
|
+
return !!value && value.length <= 120;
|
|
203
|
+
};
|
|
204
|
+
const stableDataAttr = (n) => {
|
|
205
|
+
const attrs = n.attributes;
|
|
206
|
+
if (!attrs) return null;
|
|
207
|
+
let fallback = null;
|
|
208
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
209
|
+
const name = attrs[i].name;
|
|
210
|
+
const value = attrs[i].value;
|
|
211
|
+
if (!usableDataAttr(name, value)) continue;
|
|
212
|
+
if (TEST_DATA_ATTRS2.indexOf(name.toLowerCase()) !== -1) return { name, value };
|
|
213
|
+
if (!fallback) fallback = { name, value };
|
|
214
|
+
}
|
|
215
|
+
return fallback;
|
|
216
|
+
};
|
|
217
|
+
const docRoleCount = (role) => {
|
|
218
|
+
let c = 0;
|
|
219
|
+
for (let i = 0; i < nodes.length; i++) if (roleOf(nodes[i]) === role) c++;
|
|
220
|
+
return c;
|
|
221
|
+
};
|
|
222
|
+
const rawText = (n) => (n.textContent || "").replace(/\s+/g, " ").trim();
|
|
223
|
+
const namesRatherThanReports = (t) => {
|
|
224
|
+
const letters = t.replace(/[^A-Za-z]/g, "").length;
|
|
225
|
+
if (letters < 2) return false;
|
|
226
|
+
const digits = t.replace(/[^0-9]/g, "").length;
|
|
227
|
+
return digits <= letters;
|
|
228
|
+
};
|
|
229
|
+
const usableDiscriminator = (t) => !!t && t.length <= 60 && t !== targetText && namesRatherThanReports(t);
|
|
230
|
+
const discriminatingText = (anc) => {
|
|
231
|
+
const heading = anc.querySelector('h1,h2,h3,h4,h5,h6,[role="heading"]');
|
|
232
|
+
if (heading) {
|
|
233
|
+
const t = rawText(heading);
|
|
234
|
+
if (usableDiscriminator(t)) return t;
|
|
235
|
+
}
|
|
236
|
+
const els = anc.querySelectorAll("*");
|
|
237
|
+
if (els.length > 200) return null;
|
|
238
|
+
for (let i = 0; i < els.length; i++) {
|
|
239
|
+
const n = els[i];
|
|
240
|
+
if (n === el || n.children.length > 0) continue;
|
|
241
|
+
const t = rawText(n);
|
|
242
|
+
if (!usableDiscriminator(t)) continue;
|
|
243
|
+
return t;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
};
|
|
247
|
+
const filterMatchCount = (role, text) => {
|
|
248
|
+
const needle = text.toLowerCase();
|
|
249
|
+
let c = 0;
|
|
250
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
251
|
+
if (roleOf(nodes[i]) !== role) continue;
|
|
252
|
+
if (normText(nodes[i]).indexOf(needle) !== -1) c++;
|
|
253
|
+
}
|
|
254
|
+
return c;
|
|
255
|
+
};
|
|
256
|
+
let node = el.parentElement;
|
|
257
|
+
let depth = 0;
|
|
258
|
+
while (node && depth < 12 && ancestors.length < 4) {
|
|
259
|
+
depth++;
|
|
260
|
+
const tag = (node.tagName || "").toLowerCase();
|
|
261
|
+
if (tag === "body" || tag === "html") break;
|
|
262
|
+
const testId = node.getAttribute("data-testid");
|
|
263
|
+
const id = node.getAttribute("id");
|
|
264
|
+
const explicitRole = node.getAttribute("role");
|
|
265
|
+
const ariaLabel = node.getAttribute("aria-label");
|
|
266
|
+
const anchorRole = explicitRole || (CONTAINER_TAGS.includes(tag) ? tagRoles[tag] : null) || null;
|
|
267
|
+
const dataAttr = stableDataAttr(node);
|
|
268
|
+
if (testId || id || anchorRole || ariaLabel || dataAttr) {
|
|
269
|
+
let scopedRoleCount = -1;
|
|
270
|
+
if (rolesUsable) {
|
|
271
|
+
const scoped = node.querySelectorAll(roleSources);
|
|
272
|
+
if (scoped.length <= 2e3) {
|
|
273
|
+
scopedRoleCount = 0;
|
|
274
|
+
for (let i = 0; i < scoped.length; i++) {
|
|
275
|
+
const n = scoped[i];
|
|
276
|
+
if (roleOf(n) !== targetRole) continue;
|
|
277
|
+
if (targetLevel != null && levelOf(n) !== targetLevel) continue;
|
|
278
|
+
scopedRoleCount++;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const scopedTextCount = countTextOwners(node, 2e3);
|
|
283
|
+
let filterText = null;
|
|
284
|
+
try {
|
|
285
|
+
if (anchorRole && nodesUsable) filterText = discriminatingText(node);
|
|
286
|
+
} catch {
|
|
287
|
+
filterText = null;
|
|
288
|
+
}
|
|
289
|
+
ancestors.push({
|
|
290
|
+
tag,
|
|
291
|
+
depth,
|
|
292
|
+
testId: testId || null,
|
|
293
|
+
id: id || null,
|
|
294
|
+
role: explicitRole || null,
|
|
295
|
+
ariaLabel: ariaLabel || null,
|
|
296
|
+
...scopedRoleCount >= 0 ? { scopedRoleCount } : {},
|
|
297
|
+
...scopedTextCount >= 0 ? { scopedTextCount } : {},
|
|
298
|
+
...testId ? { testIdCount: count(`[data-testid=${JSON.stringify(testId)}]`) } : {},
|
|
299
|
+
...id ? { idCount: count(`#${cssEsc(id)}`) } : {},
|
|
300
|
+
...anchorRole && nodesUsable ? { roleCount: docRoleCount(anchorRole) } : {},
|
|
301
|
+
...filterText ? { filterText, filterRoleCount: filterMatchCount(anchorRole, filterText) } : {},
|
|
302
|
+
...dataAttr ? { dataAttr, dataAttrCount: count(`[${dataAttr.name}=${JSON.stringify(dataAttr.value)}]`) } : {}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
node = node.parentElement;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
109
310
|
}
|
|
110
|
-
|
|
311
|
+
const hasLabel = !!(el.labels && el.labels.length > 0);
|
|
312
|
+
const labelText = includeLabelText ? hasLabel ? (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null : null : void 0;
|
|
313
|
+
return {
|
|
314
|
+
tagName: el.tagName?.toLowerCase?.() ?? "unknown",
|
|
315
|
+
attributes: attrMap,
|
|
316
|
+
// Collapse whitespace so multi-line text can't produce a getByText
|
|
317
|
+
// suggestion with literal newlines in it.
|
|
318
|
+
textContent: (el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80),
|
|
319
|
+
center: {
|
|
320
|
+
x: Math.round(r.x + r.width / 2),
|
|
321
|
+
y: Math.round(r.y + r.height / 2)
|
|
322
|
+
},
|
|
323
|
+
hasLabel,
|
|
324
|
+
...includeLabelText ? { labelText } : {},
|
|
325
|
+
selectorCounts,
|
|
326
|
+
...includeStructural ? { rolePosition, ancestors } : {}
|
|
327
|
+
};
|
|
111
328
|
}
|
|
112
329
|
|
|
113
|
-
// ../
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"getByTitle",
|
|
122
|
-
"locator"
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
// ../packages/core/src/locator-generation.ts
|
|
126
|
-
var TAG_TO_ROLE = {
|
|
127
|
-
a: "link",
|
|
128
|
-
button: "button",
|
|
129
|
-
nav: "navigation",
|
|
130
|
-
main: "main",
|
|
131
|
-
article: "article",
|
|
132
|
-
section: "region",
|
|
133
|
-
form: "form",
|
|
134
|
-
img: "img",
|
|
135
|
-
figure: "figure",
|
|
136
|
-
figcaption: "caption",
|
|
137
|
-
blockquote: "blockquote",
|
|
138
|
-
table: "table",
|
|
139
|
-
ul: "list",
|
|
140
|
-
ol: "list",
|
|
141
|
-
li: "listitem",
|
|
142
|
-
dialog: "dialog",
|
|
143
|
-
output: "status",
|
|
144
|
-
progress: "progressbar",
|
|
145
|
-
meter: "meter",
|
|
146
|
-
textarea: "textbox",
|
|
147
|
-
h1: "heading",
|
|
148
|
-
h2: "heading",
|
|
149
|
-
h3: "heading",
|
|
150
|
-
h4: "heading",
|
|
151
|
-
h5: "heading",
|
|
152
|
-
h6: "heading",
|
|
153
|
-
details: "group",
|
|
154
|
-
summary: "button",
|
|
155
|
-
search: "search"
|
|
156
|
-
};
|
|
157
|
-
var INPUT_TYPE_TO_ROLE = {
|
|
158
|
-
button: "button",
|
|
159
|
-
submit: "button",
|
|
160
|
-
reset: "button",
|
|
161
|
-
image: "button",
|
|
162
|
-
checkbox: "checkbox",
|
|
163
|
-
radio: "radio",
|
|
164
|
-
range: "slider",
|
|
165
|
-
search: "searchbox",
|
|
166
|
-
number: "spinbutton",
|
|
167
|
-
text: "textbox",
|
|
168
|
-
email: "textbox",
|
|
169
|
-
tel: "textbox",
|
|
170
|
-
url: "textbox",
|
|
171
|
-
password: "textbox"
|
|
172
|
-
};
|
|
173
|
-
var CAPTURED_ATTRIBUTES = [
|
|
174
|
-
"id",
|
|
175
|
-
"class",
|
|
176
|
-
"name",
|
|
177
|
-
"data-testid",
|
|
178
|
-
"placeholder",
|
|
179
|
-
"alt",
|
|
180
|
-
"title",
|
|
181
|
-
"aria-label",
|
|
182
|
-
"aria-level",
|
|
183
|
-
"role",
|
|
184
|
-
"type",
|
|
185
|
-
"href",
|
|
186
|
-
"multiple"
|
|
187
|
-
];
|
|
188
|
-
function resolveAriaRole(attrs) {
|
|
189
|
-
const explicit = attrs.attributes["role"];
|
|
190
|
-
if (explicit) return explicit;
|
|
191
|
-
const tag = attrs.tagName;
|
|
192
|
-
if (!tag) return null;
|
|
193
|
-
if (tag === "input") {
|
|
194
|
-
const type = (attrs.attributes["type"] ?? "text").toLowerCase();
|
|
195
|
-
return INPUT_TYPE_TO_ROLE[type] ?? "textbox";
|
|
196
|
-
}
|
|
197
|
-
if (tag === "select") {
|
|
198
|
-
return attrs.attributes["multiple"] != null ? "listbox" : "combobox";
|
|
199
|
-
}
|
|
200
|
-
if (tag === "a") {
|
|
201
|
-
return attrs.attributes["href"] != null ? "link" : null;
|
|
330
|
+
// ../picker-dom/src/overlay-element.ts
|
|
331
|
+
function installPickerOverlay(arg) {
|
|
332
|
+
const g = globalThis;
|
|
333
|
+
const doc = g.document;
|
|
334
|
+
if (!doc || !doc.body) {
|
|
335
|
+
if (arg.transport === "postMessage") g.parent.postMessage({ type: "pickerClosed" }, "*");
|
|
336
|
+
else g.__piwiPickState = "skipped";
|
|
337
|
+
return;
|
|
202
338
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
className
|
|
222
|
-
))
|
|
223
|
-
return 25;
|
|
224
|
-
if (/__/.test(className) || /--/.test(className)) return 35;
|
|
225
|
-
if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
|
|
226
|
-
if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
|
|
227
|
-
if (className.includes("_")) return 15;
|
|
228
|
-
if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
|
|
229
|
-
return 15;
|
|
230
|
-
}
|
|
231
|
-
function isAutoGenerated(value) {
|
|
232
|
-
if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
|
|
233
|
-
if (/^[a-f0-9]{8,}$/i.test(value)) return true;
|
|
234
|
-
if (/^[a-z]+-\d+$/.test(value)) return true;
|
|
235
|
-
if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
|
|
236
|
-
if (value.startsWith("ng-")) return true;
|
|
237
|
-
if (/^(radix-|headlessui-|mui-|mantine-|chakra-)/i.test(value)) return true;
|
|
238
|
-
if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
|
|
239
|
-
return false;
|
|
240
|
-
}
|
|
241
|
-
function generateAlternatives(attrs) {
|
|
242
|
-
const alts = [];
|
|
243
|
-
const seen = /* @__PURE__ */ new Set();
|
|
244
|
-
const add = (loc) => {
|
|
245
|
-
if (!seen.has(loc.locator)) {
|
|
246
|
-
seen.add(loc.locator);
|
|
247
|
-
alts.push(loc);
|
|
339
|
+
const Z = 2147483600;
|
|
340
|
+
const highlight = doc.createElement("div");
|
|
341
|
+
highlight.id = "__piwi_picker_highlight";
|
|
342
|
+
highlight.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};display:none;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),inset 0 0 0 1px rgba(255,255,255,.5);`;
|
|
343
|
+
const banner = doc.createElement("div");
|
|
344
|
+
banner.id = "__piwi_picker_banner";
|
|
345
|
+
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;border:1px solid #312e81;padding:10px 16px;border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,.5);max-width:min(680px,86vw);`;
|
|
346
|
+
const hlTokens = (expr) => {
|
|
347
|
+
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
348
|
+
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
349
|
+
let html = "";
|
|
350
|
+
let last = 0;
|
|
351
|
+
let m;
|
|
352
|
+
while ((m = re.exec(expr)) !== null) {
|
|
353
|
+
if (m.index > last) html += escHtml(expr.slice(last, m.index));
|
|
354
|
+
const color = m[1] ? "#86efac" : m[2] ? "#d8b4fe" : m[3] ? "#93c5fd" : m[4] ? "#fcd34d" : "#9ca3af";
|
|
355
|
+
html += `<span style="color:${color}">${escHtml(m[0])}</span>`;
|
|
356
|
+
last = re.lastIndex;
|
|
248
357
|
}
|
|
358
|
+
if (last < expr.length) html += escHtml(expr.slice(last));
|
|
359
|
+
return html;
|
|
249
360
|
};
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
add({
|
|
279
|
-
locator: `getByRole('${role}', { name: '${esc(ariaLabel)}'${levelPart} })`,
|
|
280
|
-
method: "getByRole",
|
|
281
|
-
args: withLevel({ role, name: ariaLabel }),
|
|
282
|
-
score: 85
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
if (accessibleName && ["input", "select", "textarea"].includes(tag)) {
|
|
286
|
-
const label = attr(attrs, "aria-label");
|
|
287
|
-
const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
|
|
288
|
-
if (labelBacked) {
|
|
289
|
-
add({
|
|
290
|
-
locator: `getByLabel('${esc(accessibleName)}')`,
|
|
291
|
-
method: "getByLabel",
|
|
292
|
-
args: { label: accessibleName },
|
|
293
|
-
score: 85
|
|
294
|
-
});
|
|
361
|
+
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
362
|
+
const hlLocator = (expr) => `<code style="font-family:${MONO}">${hlTokens(expr)}</code>`;
|
|
363
|
+
const head = doc.createElement("div");
|
|
364
|
+
head.innerHTML = arg.transport === "postMessage" ? "Click an element to generate locators" : 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";
|
|
365
|
+
const locatorLine = doc.createElement("div");
|
|
366
|
+
locatorLine.id = "__piwi_picker_locator";
|
|
367
|
+
locatorLine.style.cssText = `display:none;margin-top:7px;padding:5px 9px;border-radius:7px;background:#0b1120;border:1px solid #4c1d95;font:13.5px/1.55 ${MONO};word-break:break-word;overflow-wrap:anywhere;`;
|
|
368
|
+
const foot = doc.createElement("div");
|
|
369
|
+
foot.id = "__piwi_picker_foot";
|
|
370
|
+
foot.style.cssText = "color:#9ca3af;margin-top:6px;font-size:12px;";
|
|
371
|
+
foot.textContent = "\u2191 parent \xB7 \u2193 child \xB7 Esc skip";
|
|
372
|
+
banner.appendChild(head);
|
|
373
|
+
banner.appendChild(locatorLine);
|
|
374
|
+
banner.appendChild(foot);
|
|
375
|
+
const label = doc.createElement("div");
|
|
376
|
+
label.id = "__piwi_picker_label";
|
|
377
|
+
label.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(620px,92vw);background:#0b1120;color:#f9fafb;border:1px solid #7c3aed;border-radius:7px;padding:4px 8px;font:12.5px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
|
|
378
|
+
doc.body.appendChild(highlight);
|
|
379
|
+
doc.body.appendChild(label);
|
|
380
|
+
doc.body.appendChild(banner);
|
|
381
|
+
const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
382
|
+
const describe = (el) => {
|
|
383
|
+
const tag = (el.tagName || "?").toLowerCase();
|
|
384
|
+
const testId = el.getAttribute && el.getAttribute("data-testid");
|
|
385
|
+
if (testId) return `getByTestId('${escJs(testId)}')`;
|
|
386
|
+
if (el.labels && el.labels.length > 0) {
|
|
387
|
+
const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
388
|
+
if (labelText) return `getByLabel('${escJs(labelText)}')`;
|
|
295
389
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
add({
|
|
337
|
-
locator: `getByAltText('${esc(alt)}')`,
|
|
338
|
-
method: "getByAltText",
|
|
339
|
-
args: { text: alt },
|
|
340
|
-
score: 60
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
const title = attr(attrs, "title");
|
|
344
|
-
if (title) {
|
|
345
|
-
add({
|
|
346
|
-
locator: `getByTitle('${esc(title)}')`,
|
|
347
|
-
method: "getByTitle",
|
|
348
|
-
args: { title },
|
|
349
|
-
score: 50
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
const hasOwnTestId = !!(testId && isUnique(counts?.testId));
|
|
353
|
-
if (role && !hasOwnTestId) {
|
|
354
|
-
const rolePart = level != null ? `'${role}', { level: ${level} }` : `'${role}'`;
|
|
355
|
-
const leafArgs = withLevel({ role });
|
|
356
|
-
let testIdAnchorDone = false;
|
|
357
|
-
let idAnchorDone = false;
|
|
358
|
-
let roleAnchorDone = false;
|
|
359
|
-
for (const anc of attrs.ancestors ?? []) {
|
|
360
|
-
if (anc.scopedRoleCount !== 1) continue;
|
|
361
|
-
if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
|
|
362
|
-
testIdAnchorDone = true;
|
|
363
|
-
add({
|
|
364
|
-
locator: `getByTestId('${esc(anc.testId)}').getByRole(${rolePart})`,
|
|
365
|
-
method: "getByRole",
|
|
366
|
-
args: { ...leafArgs, anchorTestId: anc.testId },
|
|
367
|
-
score: 72
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
371
|
-
idAnchorDone = true;
|
|
372
|
-
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
373
|
-
add({
|
|
374
|
-
locator: `locator('${esc(anchorSelector)}').getByRole(${rolePart})`,
|
|
375
|
-
method: "getByRole",
|
|
376
|
-
args: { ...leafArgs, anchorSelector },
|
|
377
|
-
score: 64
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
|
|
381
|
-
if (!roleAnchorDone && ancestorRole && ancestorRole !== role && anc.roleCount === 1) {
|
|
382
|
-
roleAnchorDone = true;
|
|
383
|
-
add({
|
|
384
|
-
locator: `getByRole('${esc(ancestorRole)}').getByRole(${rolePart})`,
|
|
385
|
-
method: "getByRole",
|
|
386
|
-
args: { ...leafArgs, anchorRole: ancestorRole },
|
|
387
|
-
score: 55
|
|
388
|
-
});
|
|
390
|
+
const ariaLabel = el.getAttribute && el.getAttribute("aria-label");
|
|
391
|
+
if (ariaLabel) return `getByLabel('${escJs(ariaLabel)}')`;
|
|
392
|
+
const placeholder = el.getAttribute && el.getAttribute("placeholder");
|
|
393
|
+
if (placeholder) return `getByPlaceholder('${escJs(placeholder)}')`;
|
|
394
|
+
const alt = el.getAttribute && el.getAttribute("alt");
|
|
395
|
+
if (alt) return `getByAltText('${escJs(alt)}')`;
|
|
396
|
+
const titleAttr = el.getAttribute && el.getAttribute("title");
|
|
397
|
+
if (titleAttr) return `getByTitle('${escJs(titleAttr)}')`;
|
|
398
|
+
if (el.id) return `locator('#${escJs(el.id)}')`;
|
|
399
|
+
const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
|
|
400
|
+
return cls ? `locator('.${escJs(cls)}')` : tag;
|
|
401
|
+
};
|
|
402
|
+
const buildChain = (raw) => {
|
|
403
|
+
const chain2 = [];
|
|
404
|
+
let node = raw;
|
|
405
|
+
while (node && chain2.length < 15) {
|
|
406
|
+
const tag = (node.tagName || "").toLowerCase();
|
|
407
|
+
if (tag === "body" || tag === "html") break;
|
|
408
|
+
chain2.push(node);
|
|
409
|
+
node = node.parentElement;
|
|
410
|
+
}
|
|
411
|
+
return chain2.length ? chain2 : [raw];
|
|
412
|
+
};
|
|
413
|
+
const ACTIONABLE_TAGS = ["button", "a", "input", "select", "textarea", "summary", "option"];
|
|
414
|
+
const snapIndex = (chain2) => {
|
|
415
|
+
for (let i = 0; i < Math.min(chain2.length, 4); i++) {
|
|
416
|
+
const el = chain2[i];
|
|
417
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
418
|
+
if (ACTIONABLE_TAGS.includes(tag)) return i;
|
|
419
|
+
if (el.getAttribute && (el.getAttribute("role") || el.getAttribute("data-testid"))) return i;
|
|
420
|
+
}
|
|
421
|
+
return 0;
|
|
422
|
+
};
|
|
423
|
+
const locatorOf = (el) => {
|
|
424
|
+
const hook = g.__piwiDescribeElement;
|
|
425
|
+
if (typeof hook === "function") {
|
|
426
|
+
try {
|
|
427
|
+
const derived = hook(el);
|
|
428
|
+
if (typeof derived === "string" && derived) return derived;
|
|
429
|
+
} catch {
|
|
389
430
|
}
|
|
390
431
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
432
|
+
return describe(el);
|
|
433
|
+
};
|
|
434
|
+
let chain = [];
|
|
435
|
+
let idx = 0;
|
|
436
|
+
let lastRaw = null;
|
|
437
|
+
let labeled = null;
|
|
438
|
+
const placeLabel = (r) => {
|
|
439
|
+
label.style.display = "block";
|
|
440
|
+
const lr = label.getBoundingClientRect();
|
|
441
|
+
const vw = g.innerWidth || doc.documentElement.clientWidth || 0;
|
|
442
|
+
const vh = g.innerHeight || doc.documentElement.clientHeight || 0;
|
|
443
|
+
let top = r.top - lr.height - 6;
|
|
444
|
+
if (top < 4) top = r.bottom + 6;
|
|
445
|
+
if (top + lr.height > vh - 4) top = Math.max(4, vh - lr.height - 4);
|
|
446
|
+
let left = r.left;
|
|
447
|
+
if (left + lr.width > vw - 6) left = vw - lr.width - 6;
|
|
448
|
+
if (left < 6) left = 6;
|
|
449
|
+
label.style.left = left + "px";
|
|
450
|
+
label.style.top = top + "px";
|
|
451
|
+
};
|
|
452
|
+
const current = () => chain[idx] ?? null;
|
|
453
|
+
const refresh = () => {
|
|
454
|
+
const el = current();
|
|
455
|
+
if (!el) {
|
|
456
|
+
highlight.style.display = "none";
|
|
457
|
+
label.style.display = "none";
|
|
458
|
+
return;
|
|
399
459
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
460
|
+
const r = el.getBoundingClientRect();
|
|
461
|
+
highlight.style.display = "block";
|
|
462
|
+
highlight.style.left = r.left + "px";
|
|
463
|
+
highlight.style.top = r.top + "px";
|
|
464
|
+
highlight.style.width = r.width + "px";
|
|
465
|
+
highlight.style.height = r.height + "px";
|
|
466
|
+
if (el !== labeled) {
|
|
467
|
+
labeled = el;
|
|
468
|
+
const tag = (el.tagName || "?").toLowerCase();
|
|
469
|
+
const locatorHtml = hlTokens(locatorOf(el));
|
|
470
|
+
label.innerHTML = `<span style="color:#c4b5fd"><${tag}></span> ${locatorHtml}`;
|
|
471
|
+
locatorLine.innerHTML = locatorHtml;
|
|
472
|
+
locatorLine.style.display = "block";
|
|
412
473
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
function approximateAccessibleName(attrs) {
|
|
417
|
-
const a = attrs.attributes;
|
|
418
|
-
const ariaLabel = a["aria-label"];
|
|
419
|
-
if (ariaLabel) return ariaLabel;
|
|
420
|
-
if (attrs.textContent) return attrs.textContent;
|
|
421
|
-
const title = a["title"];
|
|
422
|
-
if (title) return title;
|
|
423
|
-
const placeholder = a["placeholder"];
|
|
424
|
-
if (placeholder) return placeholder;
|
|
425
|
-
return null;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// src/internal/capture/locator-healing.ts
|
|
429
|
-
function dedupeSnapshotsByLocation(snaps) {
|
|
430
|
-
const lastWithElement = /* @__PURE__ */ new Map();
|
|
431
|
-
const lastAny = /* @__PURE__ */ new Map();
|
|
432
|
-
snaps.forEach((s, i) => {
|
|
433
|
-
if (!s.location) return;
|
|
434
|
-
lastAny.set(s.location, i);
|
|
435
|
-
if (s.element) lastWithElement.set(s.location, i);
|
|
436
|
-
});
|
|
437
|
-
return snaps.filter((s, i) => {
|
|
438
|
-
if (!s.location) return true;
|
|
439
|
-
return (lastWithElement.get(s.location) ?? lastAny.get(s.location)) === i;
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
var LOCATOR_METHODS = [...LOCATOR_BUILDER_METHODS];
|
|
443
|
-
var CHAIN_METHODS = [
|
|
444
|
-
"first",
|
|
445
|
-
"nth",
|
|
446
|
-
"last",
|
|
447
|
-
"filter",
|
|
448
|
-
"and",
|
|
449
|
-
"or",
|
|
450
|
-
"locator",
|
|
451
|
-
"getByRole",
|
|
452
|
-
"getByTestId",
|
|
453
|
-
"getByText",
|
|
454
|
-
"getByLabel",
|
|
455
|
-
"getByPlaceholder",
|
|
456
|
-
"getByAltText",
|
|
457
|
-
"getByTitle"
|
|
458
|
-
];
|
|
459
|
-
var ACTION_METHODS = [
|
|
460
|
-
"click",
|
|
461
|
-
"fill",
|
|
462
|
-
"check",
|
|
463
|
-
"uncheck",
|
|
464
|
-
"selectOption",
|
|
465
|
-
"dblclick",
|
|
466
|
-
"tap",
|
|
467
|
-
"hover",
|
|
468
|
-
"press",
|
|
469
|
-
"type",
|
|
470
|
-
"pressSequentially",
|
|
471
|
-
"clear",
|
|
472
|
-
"setInputFiles",
|
|
473
|
-
"dragTo",
|
|
474
|
-
"focus",
|
|
475
|
-
"blur",
|
|
476
|
-
"scrollIntoViewIfNeeded",
|
|
477
|
-
"dispatchEvent",
|
|
478
|
-
"selectText",
|
|
479
|
-
// Not an action, but a successful waitFor proves the element resolved — the
|
|
480
|
-
// closest capture hook available for assertion-style usage of a locator.
|
|
481
|
-
"waitFor"
|
|
482
|
-
];
|
|
483
|
-
var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
|
|
484
|
-
var EXPECT_METHOD = "_expect";
|
|
485
|
-
var EXPECT_CAPTURE_EXPRESSIONS = /* @__PURE__ */ new Set([
|
|
486
|
-
"to.be.attached",
|
|
487
|
-
"to.be.checked",
|
|
488
|
-
"to.be.disabled",
|
|
489
|
-
"to.be.editable",
|
|
490
|
-
"to.be.empty",
|
|
491
|
-
"to.be.enabled",
|
|
492
|
-
"to.be.focused",
|
|
493
|
-
"to.be.in.viewport",
|
|
494
|
-
"to.be.readonly",
|
|
495
|
-
"to.be.visible",
|
|
496
|
-
"to.contain.class",
|
|
497
|
-
"to.contain.text",
|
|
498
|
-
"to.have.accessible.description",
|
|
499
|
-
"to.have.accessible.error.message",
|
|
500
|
-
"to.have.accessible.name",
|
|
501
|
-
"to.have.attribute",
|
|
502
|
-
"to.have.attribute.value",
|
|
503
|
-
"to.have.class",
|
|
504
|
-
"to.have.css",
|
|
505
|
-
"to.have.id",
|
|
506
|
-
"to.have.js.property",
|
|
507
|
-
"to.have.role",
|
|
508
|
-
"to.have.text",
|
|
509
|
-
"to.have.value",
|
|
510
|
-
"to.match.aria"
|
|
511
|
-
]);
|
|
512
|
-
function extractAccessibleName(ariaSnapshot) {
|
|
513
|
-
if (!ariaSnapshot) return null;
|
|
514
|
-
const match = ariaSnapshot.match(/- \w+ "([^"]+)"/);
|
|
515
|
-
if (match) return match[1];
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
|
|
519
|
-
"getByText",
|
|
520
|
-
"getByRole",
|
|
521
|
-
"getByLabel",
|
|
522
|
-
"getByPlaceholder",
|
|
523
|
-
"getByTitle",
|
|
524
|
-
"getByAltText"
|
|
525
|
-
]);
|
|
526
|
-
var escAttr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
527
|
-
var SUGG_TEXT_ROLES = /* @__PURE__ */ new Set([
|
|
528
|
-
"button",
|
|
529
|
-
"link",
|
|
530
|
-
"heading",
|
|
531
|
-
"menuitem",
|
|
532
|
-
"tab",
|
|
533
|
-
"option",
|
|
534
|
-
"cell",
|
|
535
|
-
"columnheader",
|
|
536
|
-
"rowheader",
|
|
537
|
-
"gridcell",
|
|
538
|
-
"treeitem",
|
|
539
|
-
"listitem",
|
|
540
|
-
"checkbox",
|
|
541
|
-
"radio",
|
|
542
|
-
"switch"
|
|
543
|
-
]);
|
|
544
|
-
var SUGG_FIELD_ROLES = /* @__PURE__ */ new Set(["textbox", "combobox", "searchbox", "spinbutton", "slider"]);
|
|
545
|
-
function failedNameAndRole(failed) {
|
|
546
|
-
if (failed.method === "getByRole") {
|
|
547
|
-
const role = typeof failed.args[0] === "string" ? failed.args[0] : null;
|
|
548
|
-
const opts = failed.args[1];
|
|
549
|
-
const name = opts && typeof opts.name === "string" ? opts.name : null;
|
|
550
|
-
const level = opts && typeof opts.level === "number" ? opts.level : null;
|
|
551
|
-
return { role, name, level };
|
|
552
|
-
}
|
|
553
|
-
const first = failed.args.find((a) => typeof a === "string");
|
|
554
|
-
return { role: null, name: typeof first === "string" ? first : null, level: null };
|
|
555
|
-
}
|
|
556
|
-
function renderFailing(failed) {
|
|
557
|
-
const { role, name } = failedNameAndRole(failed);
|
|
558
|
-
if (failed.method === "getByRole") {
|
|
559
|
-
return name ? `getByRole('${escAttr(role ?? "")}', { name: '${escAttr(name)}' })` : `getByRole('${escAttr(role ?? "")}')`;
|
|
560
|
-
}
|
|
561
|
-
return `${failed.method}('${escAttr(name ?? "")}')`;
|
|
562
|
-
}
|
|
563
|
-
function freshSuggestions(candidate, failedMethod) {
|
|
564
|
-
const out = [];
|
|
565
|
-
const role = candidate.role;
|
|
566
|
-
const name = candidate.name;
|
|
567
|
-
const push = (s) => {
|
|
568
|
-
if (!out.includes(s)) out.push(s);
|
|
474
|
+
placeLabel(r);
|
|
475
|
+
foot.textContent = "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip";
|
|
569
476
|
};
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
if (
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
477
|
+
const stop = (e) => {
|
|
478
|
+
e.preventDefault();
|
|
479
|
+
e.stopImmediatePropagation();
|
|
480
|
+
};
|
|
481
|
+
const isOwn = (el) => el === banner || el === highlight || el === label || banner.contains && banner.contains(el) || !!(el && el.__piwiHint);
|
|
482
|
+
let bannerDocked = "top";
|
|
483
|
+
const dockBanner = (side) => {
|
|
484
|
+
if (bannerDocked === side) return;
|
|
485
|
+
bannerDocked = side;
|
|
486
|
+
if (side === "bottom") {
|
|
487
|
+
banner.style.top = "auto";
|
|
488
|
+
banner.style.bottom = "12px";
|
|
489
|
+
} else {
|
|
490
|
+
banner.style.top = "12px";
|
|
491
|
+
banner.style.bottom = "auto";
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
const onMove = (e) => {
|
|
495
|
+
const raw = e.target;
|
|
496
|
+
if (!raw || isOwn(raw)) {
|
|
497
|
+
highlight.style.display = "none";
|
|
498
|
+
label.style.display = "none";
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (raw !== lastRaw) {
|
|
502
|
+
lastRaw = raw;
|
|
503
|
+
chain = buildChain(raw);
|
|
504
|
+
idx = snapIndex(chain);
|
|
505
|
+
}
|
|
506
|
+
refresh();
|
|
507
|
+
const el = current();
|
|
508
|
+
if (el) {
|
|
509
|
+
const r = el.getBoundingClientRect();
|
|
510
|
+
const br = banner.getBoundingClientRect();
|
|
511
|
+
const margin = 8;
|
|
512
|
+
if (r.left < br.right + margin && r.right > br.left - margin && r.top < br.bottom + margin && r.bottom > br.top - margin) {
|
|
513
|
+
dockBanner(bannerDocked === "top" ? "bottom" : "top");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
const suppressed = ["mousedown", "mouseup", "pointerdown", "pointerup", "auxclick", "dblclick"];
|
|
518
|
+
const removePickingListeners = () => {
|
|
519
|
+
doc.removeEventListener("mousemove", onMove, true);
|
|
520
|
+
doc.removeEventListener("click", onClick, true);
|
|
521
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
522
|
+
if (arg.transport === "postMessage") g.removeEventListener("message", onParentMsg, false);
|
|
523
|
+
};
|
|
524
|
+
const removeSuppressed = () => {
|
|
525
|
+
for (const t of suppressed) doc.removeEventListener(t, stop, true);
|
|
526
|
+
};
|
|
527
|
+
const cleanup = () => {
|
|
528
|
+
highlight.remove();
|
|
529
|
+
label.remove();
|
|
530
|
+
banner.remove();
|
|
531
|
+
};
|
|
532
|
+
const reportPicked = (el) => {
|
|
533
|
+
removePickingListeners();
|
|
534
|
+
highlight.style.display = "none";
|
|
535
|
+
label.style.display = "none";
|
|
536
|
+
if (arg.transport === "postMessage") {
|
|
537
|
+
const probeFn = g.__piwiProbe;
|
|
538
|
+
const attrs = typeof probeFn === "function" ? probeFn(el, arg.probeArg) : null;
|
|
539
|
+
g.__piwiSnapshotExtras?.onPick?.();
|
|
540
|
+
doc.addEventListener("click", stop, true);
|
|
541
|
+
doc.addEventListener("keydown", stop, true);
|
|
542
|
+
foot.textContent = "Analyzing element\u2026";
|
|
543
|
+
g.parent.postMessage({ type: "elementPicked", attrs }, "*");
|
|
544
|
+
} else {
|
|
545
|
+
removeSuppressed();
|
|
546
|
+
g.__piwiPickedElement = el;
|
|
547
|
+
g.__piwiPickState = "picked";
|
|
548
|
+
foot.textContent = "Analyzing element\u2026";
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
const reportSkipped = () => {
|
|
552
|
+
removePickingListeners();
|
|
553
|
+
if (arg.transport === "postMessage") {
|
|
554
|
+
doc.removeEventListener("click", stop, true);
|
|
555
|
+
doc.removeEventListener("keydown", stop, true);
|
|
556
|
+
removeSuppressed();
|
|
557
|
+
g.__piwiSnapshotExtras?.onClose?.();
|
|
558
|
+
cleanup();
|
|
559
|
+
g.parent.postMessage({ type: "pickerClosed" }, "*");
|
|
560
|
+
} else {
|
|
561
|
+
removeSuppressed();
|
|
562
|
+
g.__piwiPickState = "skipped";
|
|
563
|
+
cleanup();
|
|
606
564
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
565
|
+
};
|
|
566
|
+
const onClick = (e) => {
|
|
567
|
+
stop(e);
|
|
568
|
+
const el = current();
|
|
569
|
+
if (!el || isOwn(e.target)) return;
|
|
570
|
+
reportPicked(el);
|
|
571
|
+
};
|
|
572
|
+
const onKey = (e) => {
|
|
573
|
+
if (e.key === "Escape") {
|
|
574
|
+
stop(e);
|
|
575
|
+
reportSkipped();
|
|
576
|
+
return;
|
|
611
577
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
578
|
+
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
579
|
+
stop(e);
|
|
580
|
+
if (e.key === "ArrowUp") idx = Math.min(idx + 1, chain.length - 1);
|
|
581
|
+
else idx = Math.max(idx - 1, 0);
|
|
582
|
+
refresh();
|
|
616
583
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
584
|
+
};
|
|
585
|
+
const onParentMsg = (e) => {
|
|
586
|
+
const d = e.data;
|
|
587
|
+
if (!d || typeof d.type !== "string" || d.type !== "piwiPickerKey" || typeof d.key !== "string") return;
|
|
588
|
+
onKey({
|
|
589
|
+
key: d.key,
|
|
590
|
+
preventDefault() {
|
|
591
|
+
},
|
|
592
|
+
stopImmediatePropagation() {
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
};
|
|
596
|
+
g.__piwiPickCleanup = cleanup;
|
|
597
|
+
doc.addEventListener("mousemove", onMove, true);
|
|
598
|
+
doc.addEventListener("click", onClick, true);
|
|
599
|
+
doc.addEventListener("keydown", onKey, true);
|
|
600
|
+
for (const t of suppressed) doc.addEventListener(t, stop, true);
|
|
601
|
+
if (arg.transport === "postMessage") {
|
|
602
|
+
g.addEventListener("message", onParentMsg, false);
|
|
603
|
+
g.parent.postMessage({ type: "pickerReady" }, "*");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ../picker-dom/src/overlay-anchors.ts
|
|
608
|
+
function showAnchorPicker(arg) {
|
|
609
|
+
const g = globalThis;
|
|
610
|
+
const doc = g.document;
|
|
611
|
+
const el = g.__piwiPickedElement;
|
|
612
|
+
if (!doc || !doc.body || !el) {
|
|
613
|
+
g.__piwiAnchorState = "skipped";
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const Z = 2147483600;
|
|
617
|
+
const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
|
|
618
|
+
const roleOf = (n) => {
|
|
619
|
+
const explicit = n.getAttribute && n.getAttribute("role");
|
|
620
|
+
if (explicit) return explicit;
|
|
621
|
+
const tag = (n.tagName || "").toLowerCase();
|
|
622
|
+
if (tag === "input") return inputRoles[(n.getAttribute("type") || "text").toLowerCase()] ?? "textbox";
|
|
623
|
+
if (tag === "select") return n.getAttribute("multiple") != null ? "listbox" : "combobox";
|
|
624
|
+
if (tag === "a") return n.getAttribute("href") != null ? "link" : null;
|
|
625
|
+
return tagRoles[tag] ?? null;
|
|
626
|
+
};
|
|
627
|
+
const levelOf = (n) => {
|
|
628
|
+
const m = /^h([1-6])$/.exec((n.tagName || "").toLowerCase());
|
|
629
|
+
if (m) return Number(m[1]);
|
|
630
|
+
const al = n.getAttribute && n.getAttribute("aria-level");
|
|
631
|
+
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
632
|
+
};
|
|
633
|
+
const leafMatches = (scope) => {
|
|
634
|
+
try {
|
|
635
|
+
if (leafTestId) return scope.querySelectorAll(`[data-testid=${JSON.stringify(leafTestId)}]`).length;
|
|
636
|
+
const nodes = scope.querySelectorAll(roleSources);
|
|
637
|
+
if (nodes.length > 2e3) return -1;
|
|
638
|
+
let matched = 0;
|
|
639
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
640
|
+
const n = nodes[i];
|
|
641
|
+
if (roleOf(n) !== leafRole) continue;
|
|
642
|
+
if (leafLevel != null && levelOf(n) !== leafLevel) continue;
|
|
643
|
+
matched++;
|
|
644
|
+
}
|
|
645
|
+
return matched;
|
|
646
|
+
} catch {
|
|
647
|
+
return -1;
|
|
621
648
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
649
|
+
};
|
|
650
|
+
let roleNodes = [];
|
|
651
|
+
try {
|
|
652
|
+
const all = doc.querySelectorAll(roleSources);
|
|
653
|
+
if (all.length <= 4e3) roleNodes = Array.from(all);
|
|
654
|
+
} catch {
|
|
655
|
+
roleNodes = [];
|
|
656
|
+
}
|
|
657
|
+
const count = (sel) => {
|
|
658
|
+
try {
|
|
659
|
+
return doc.querySelectorAll(sel).length;
|
|
660
|
+
} catch {
|
|
661
|
+
return void 0;
|
|
625
662
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
663
|
+
};
|
|
664
|
+
const rows = [];
|
|
665
|
+
let node = el.parentElement;
|
|
666
|
+
let depth = 0;
|
|
667
|
+
while (node && depth < 12) {
|
|
668
|
+
depth++;
|
|
669
|
+
const tag = (node.tagName || "").toLowerCase();
|
|
670
|
+
if (tag === "body" || tag === "html") break;
|
|
671
|
+
const testId = node.getAttribute("data-testid");
|
|
672
|
+
const id = node.getAttribute("id");
|
|
673
|
+
const ariaLabel = node.getAttribute("aria-label");
|
|
674
|
+
const role = roleOf(node);
|
|
675
|
+
const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
|
|
676
|
+
if (testId) info.testIdCount = count(`[data-testid=${JSON.stringify(testId)}]`);
|
|
677
|
+
if (id) {
|
|
678
|
+
try {
|
|
679
|
+
info.idCount = count(`#${doc.defaultView.CSS.escape(id)}`);
|
|
680
|
+
} catch {
|
|
681
|
+
}
|
|
629
682
|
}
|
|
630
|
-
if (
|
|
631
|
-
|
|
632
|
-
|
|
683
|
+
if (role) {
|
|
684
|
+
let roleCount = 0;
|
|
685
|
+
let labeledCount = 0;
|
|
686
|
+
for (const n of roleNodes) {
|
|
687
|
+
if (roleOf(n) !== role) continue;
|
|
688
|
+
roleCount++;
|
|
689
|
+
if (ariaLabel && n.getAttribute && n.getAttribute("aria-label") === ariaLabel) labeledCount++;
|
|
690
|
+
}
|
|
691
|
+
info.roleCount = roleCount;
|
|
692
|
+
if (ariaLabel) info.labeledRoleCount = labeledCount;
|
|
633
693
|
}
|
|
634
|
-
|
|
694
|
+
info.scopedLeafCount = leafMatches(node);
|
|
695
|
+
const hookLabel = testId ? `data-testid="${testId}"` : id ? `#${id}` : ariaLabel && role ? `${role} "${ariaLabel}"` : role ? `role ${role}` : "no stable hook";
|
|
696
|
+
rows.push({
|
|
697
|
+
node,
|
|
698
|
+
info,
|
|
699
|
+
hookLabel,
|
|
700
|
+
selectable: !!(testId || id || role && (ariaLabel || info.roleCount === 1))
|
|
701
|
+
});
|
|
702
|
+
node = node.parentElement;
|
|
703
|
+
}
|
|
704
|
+
if (rows.length === 0) {
|
|
705
|
+
g.__piwiAnchorState = "skipped";
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
709
|
+
const outline = doc.createElement("div");
|
|
710
|
+
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);`;
|
|
711
|
+
const outlineLabel = doc.createElement("div");
|
|
712
|
+
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);`;
|
|
713
|
+
const pickedOutline = doc.createElement("div");
|
|
714
|
+
const pr = el.getBoundingClientRect();
|
|
715
|
+
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;`;
|
|
716
|
+
const panel = doc.createElement("div");
|
|
717
|
+
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);`;
|
|
718
|
+
const title = doc.createElement("div");
|
|
719
|
+
title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
|
|
720
|
+
title.textContent = "Scope to stable parents (optional)";
|
|
721
|
+
const sub = doc.createElement("div");
|
|
722
|
+
sub.style.cssText = "color:#9ca3af;margin-bottom:10px;";
|
|
723
|
+
sub.textContent = "Pick one or more parents to anchor the locator to. Hover a row to see the parent.";
|
|
724
|
+
panel.appendChild(title);
|
|
725
|
+
panel.appendChild(sub);
|
|
726
|
+
const selected = /* @__PURE__ */ new Set();
|
|
727
|
+
const footer = doc.createElement("div");
|
|
728
|
+
footer.style.cssText = "margin:10px 0;font-weight:600;";
|
|
729
|
+
const segMatches = (scope, info) => {
|
|
635
730
|
try {
|
|
636
|
-
|
|
731
|
+
if (info.testId) return Array.from(scope.querySelectorAll(`[data-testid=${JSON.stringify(info.testId)}]`));
|
|
732
|
+
if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
|
|
733
|
+
const nodes = Array.from(scope.querySelectorAll(roleSources));
|
|
734
|
+
if (nodes.length > 2e3) return [];
|
|
735
|
+
return nodes.filter(
|
|
736
|
+
(n) => roleOf(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
|
|
737
|
+
);
|
|
637
738
|
} catch {
|
|
739
|
+
return [];
|
|
638
740
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
741
|
+
};
|
|
742
|
+
const chainCount = () => {
|
|
743
|
+
const chosen = rows.filter((_, i) => selected.has(i)).sort((a, b) => b.info.depth - a.info.depth);
|
|
744
|
+
if (chosen.length === 0) return -1;
|
|
745
|
+
let scopes = [doc];
|
|
746
|
+
for (const row of chosen) {
|
|
747
|
+
const next = [];
|
|
748
|
+
for (const s of scopes) next.push(...segMatches(s, row.info));
|
|
749
|
+
scopes = next.slice(0, 200);
|
|
750
|
+
if (scopes.length === 0) return 0;
|
|
751
|
+
}
|
|
752
|
+
let total = 0;
|
|
753
|
+
for (const s of scopes) {
|
|
754
|
+
const c = leafMatches(s);
|
|
755
|
+
if (c > 0) total += c;
|
|
756
|
+
if (total > 50) return total;
|
|
757
|
+
}
|
|
758
|
+
return total;
|
|
759
|
+
};
|
|
760
|
+
const refreshFooter = () => {
|
|
761
|
+
if (selected.size === 0) {
|
|
762
|
+
footer.textContent = "No parents selected \u2014 standard alternatives only.";
|
|
763
|
+
footer.style.color = "#9ca3af";
|
|
764
|
+
g.__piwiPickChainCount = void 0;
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const c = chainCount();
|
|
768
|
+
g.__piwiPickChainCount = c;
|
|
769
|
+
if (c === 1) {
|
|
770
|
+
footer.textContent = "\u2713 Selection matches exactly 1 element";
|
|
771
|
+
footer.style.color = "#4ade80";
|
|
772
|
+
} else {
|
|
773
|
+
footer.textContent = c < 0 ? "Match count unavailable" : `\u2717 Selection matches ${c} elements`;
|
|
774
|
+
footer.style.color = "#fbbf24";
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
rows.forEach((row, i) => {
|
|
778
|
+
const line = doc.createElement("label");
|
|
779
|
+
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"};`;
|
|
780
|
+
const box = doc.createElement("input");
|
|
781
|
+
box.type = "checkbox";
|
|
782
|
+
box.disabled = !row.selectable;
|
|
783
|
+
const text = doc.createElement("span");
|
|
784
|
+
text.style.cssText = "flex:1;min-width:0;";
|
|
785
|
+
const code = doc.createElement("code");
|
|
786
|
+
code.style.cssText = `display:block;font:12px ${MONO};color:#f3f4f6;word-break:break-all;`;
|
|
787
|
+
code.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
788
|
+
const hint = doc.createElement("span");
|
|
789
|
+
hint.style.cssText = "color:#9ca3af;";
|
|
790
|
+
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";
|
|
791
|
+
text.appendChild(code);
|
|
792
|
+
text.appendChild(hint);
|
|
793
|
+
line.appendChild(box);
|
|
794
|
+
line.appendChild(text);
|
|
795
|
+
line.addEventListener("mouseenter", () => {
|
|
796
|
+
const r = row.node.getBoundingClientRect();
|
|
797
|
+
outline.style.display = "block";
|
|
798
|
+
outline.style.left = r.left + "px";
|
|
799
|
+
outline.style.top = r.top + "px";
|
|
800
|
+
outline.style.width = r.width + "px";
|
|
801
|
+
outline.style.height = r.height + "px";
|
|
802
|
+
outlineLabel.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
803
|
+
outlineLabel.style.display = "block";
|
|
804
|
+
const lr = outlineLabel.getBoundingClientRect();
|
|
805
|
+
const vw = g.innerWidth || doc.documentElement.clientWidth || 0;
|
|
806
|
+
const top = r.top - lr.height - 6 < 4 ? r.bottom + 6 : r.top - lr.height - 6;
|
|
807
|
+
outlineLabel.style.left = Math.max(6, Math.min(r.left, vw - lr.width - 6)) + "px";
|
|
808
|
+
outlineLabel.style.top = top + "px";
|
|
809
|
+
});
|
|
810
|
+
line.addEventListener("mouseleave", () => {
|
|
811
|
+
outline.style.display = "none";
|
|
812
|
+
outlineLabel.style.display = "none";
|
|
813
|
+
});
|
|
814
|
+
box.addEventListener("change", () => {
|
|
815
|
+
if (box.checked) selected.add(i);
|
|
816
|
+
else selected.delete(i);
|
|
817
|
+
refreshFooter();
|
|
818
|
+
});
|
|
819
|
+
panel.appendChild(line);
|
|
820
|
+
});
|
|
821
|
+
panel.appendChild(footer);
|
|
822
|
+
const cleanup = () => {
|
|
823
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
824
|
+
panel.remove();
|
|
825
|
+
outline.remove();
|
|
826
|
+
outlineLabel.remove();
|
|
827
|
+
pickedOutline.remove();
|
|
828
|
+
};
|
|
829
|
+
const done = (state) => {
|
|
830
|
+
g.__piwiPickAnchors = state === "done" ? rows.filter((_, i) => selected.has(i)).map((r) => r.info) : [];
|
|
831
|
+
g.__piwiAnchorState = state;
|
|
832
|
+
cleanup();
|
|
833
|
+
};
|
|
834
|
+
const onKey = (e) => {
|
|
835
|
+
if (e.key !== "Escape") return;
|
|
836
|
+
e.preventDefault();
|
|
837
|
+
e.stopImmediatePropagation();
|
|
838
|
+
done("skipped");
|
|
839
|
+
};
|
|
840
|
+
const buttonRow = doc.createElement("div");
|
|
841
|
+
buttonRow.style.cssText = "display:flex;gap:8px;margin-top:4px;";
|
|
842
|
+
const useBtn = doc.createElement("button");
|
|
843
|
+
useBtn.style.cssText = "flex:1;background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer;font:600 12px system-ui;";
|
|
844
|
+
useBtn.textContent = "Use selected parents";
|
|
845
|
+
useBtn.addEventListener("click", (e) => {
|
|
846
|
+
e.preventDefault();
|
|
847
|
+
e.stopImmediatePropagation();
|
|
848
|
+
done(selected.size > 0 ? "done" : "skipped");
|
|
849
|
+
});
|
|
850
|
+
const skipBtn = doc.createElement("button");
|
|
851
|
+
skipBtn.style.cssText = "background:none;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:8px 10px;cursor:pointer;font:12px system-ui;";
|
|
852
|
+
skipBtn.textContent = "Skip (Esc)";
|
|
853
|
+
skipBtn.addEventListener("click", (e) => {
|
|
854
|
+
e.preventDefault();
|
|
855
|
+
e.stopImmediatePropagation();
|
|
856
|
+
done("skipped");
|
|
857
|
+
});
|
|
858
|
+
buttonRow.appendChild(useBtn);
|
|
859
|
+
buttonRow.appendChild(skipBtn);
|
|
860
|
+
panel.appendChild(buttonRow);
|
|
861
|
+
refreshFooter();
|
|
862
|
+
g.__piwiAnchorCleanup = cleanup;
|
|
863
|
+
doc.addEventListener("keydown", onKey, true);
|
|
864
|
+
doc.body.appendChild(pickedOutline);
|
|
865
|
+
doc.body.appendChild(outline);
|
|
866
|
+
doc.body.appendChild(outlineLabel);
|
|
867
|
+
doc.body.appendChild(panel);
|
|
644
868
|
}
|
|
645
869
|
|
|
646
|
-
// src/
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
locatorSuggestion: "piwi-locator-suggestion",
|
|
654
|
-
pageState: "piwi-page-state",
|
|
655
|
-
userPick: "piwi-user-pick"
|
|
656
|
-
};
|
|
657
|
-
var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
|
|
658
|
-
var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
659
|
-
var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
|
|
660
|
-
|
|
661
|
-
// src/internal/capture/inspect-on-failure.ts
|
|
662
|
-
function isCi(ci) {
|
|
663
|
-
return ci !== void 0 && ci !== "" && ci !== "false";
|
|
664
|
-
}
|
|
665
|
-
function shouldInspectOnFailure(gate) {
|
|
666
|
-
if (gate.enabled !== "true") return false;
|
|
667
|
-
if (isCi(gate.ci)) return false;
|
|
668
|
-
if (gate.headless !== false) return false;
|
|
669
|
-
if (gate.status !== "failed" && gate.status !== "timedOut") return false;
|
|
670
|
-
if (gate.status === gate.expectedStatus) return false;
|
|
671
|
-
return gate.retry >= gate.retries;
|
|
672
|
-
}
|
|
673
|
-
function environmentalSkipReason(gate) {
|
|
674
|
-
if (gate.enabled !== "true") return null;
|
|
675
|
-
if (gate.status !== "failed" && gate.status !== "timedOut") return null;
|
|
676
|
-
if (gate.status === gate.expectedStatus) return null;
|
|
677
|
-
if (isCi(gate.ci)) return "running under CI \u2014 this is a headed, local-only feature";
|
|
678
|
-
if (gate.headless !== false) {
|
|
679
|
-
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
870
|
+
// ../picker-dom/src/overlay-confirm.ts
|
|
871
|
+
function showPickerChoices(arg) {
|
|
872
|
+
const g = globalThis;
|
|
873
|
+
const doc = g.document;
|
|
874
|
+
if (!doc || !doc.body) {
|
|
875
|
+
g.__piwiPickChoice = -1;
|
|
876
|
+
return;
|
|
680
877
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
878
|
+
const Z = 2147483600;
|
|
879
|
+
const wrap = doc.createElement("div");
|
|
880
|
+
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;`;
|
|
881
|
+
const panel = doc.createElement("div");
|
|
882
|
+
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);";
|
|
883
|
+
const hlLocator = (expr) => {
|
|
884
|
+
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
885
|
+
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
886
|
+
let html = "";
|
|
887
|
+
let last = 0;
|
|
888
|
+
let m;
|
|
889
|
+
while ((m = re.exec(expr)) !== null) {
|
|
890
|
+
if (m.index > last) html += escHtml(expr.slice(last, m.index));
|
|
891
|
+
const color = m[1] ? "#86efac" : m[2] ? "#d8b4fe" : m[3] ? "#93c5fd" : m[4] ? "#fcd34d" : "#9ca3af";
|
|
892
|
+
html += `<span style="color:${color}">${escHtml(m[0])}</span>`;
|
|
893
|
+
last = re.lastIndex;
|
|
894
|
+
}
|
|
895
|
+
if (last < expr.length) html += escHtml(expr.slice(last));
|
|
896
|
+
return html;
|
|
897
|
+
};
|
|
898
|
+
const title = doc.createElement("div");
|
|
899
|
+
title.style.cssText = "font-weight:600;margin-bottom:4px;";
|
|
900
|
+
title.textContent = arg.failing ? "Pick a replacement locator" : "Pick a locator";
|
|
901
|
+
const sub = doc.createElement("div");
|
|
902
|
+
sub.style.cssText = "color:#9ca3af;margin-bottom:12px;";
|
|
903
|
+
if (arg.failing) {
|
|
904
|
+
sub.innerHTML = `Replaces <code style="font-family:ui-monospace,Menlo,monospace">${hlLocator(arg.failing)}</code> \u2014 ranked by stability score.`;
|
|
905
|
+
} else {
|
|
906
|
+
sub.textContent = "For the element you picked \u2014 ranked by stability score.";
|
|
907
|
+
}
|
|
908
|
+
panel.appendChild(title);
|
|
909
|
+
panel.appendChild(sub);
|
|
910
|
+
const done = (choice) => {
|
|
911
|
+
g.__piwiPickChoice = choice;
|
|
912
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
913
|
+
wrap.remove();
|
|
914
|
+
};
|
|
915
|
+
const onKey = (e) => {
|
|
916
|
+
if (e.key !== "Escape") return;
|
|
917
|
+
e.preventDefault();
|
|
918
|
+
e.stopImmediatePropagation();
|
|
919
|
+
done(-1);
|
|
693
920
|
};
|
|
921
|
+
arg.choices.forEach((c, i) => {
|
|
922
|
+
const btn = doc.createElement("button");
|
|
923
|
+
btn.style.cssText = "display:flex;justify-content:space-between;align-items:center;gap:12px;width:100%;text-align:left;background:#0b1120;color:#f3f4f6;border:1px solid #374151;border-radius:6px;padding:8px 12px;margin:0 0 8px;cursor:pointer;font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;";
|
|
924
|
+
const code = doc.createElement("span");
|
|
925
|
+
code.innerHTML = hlLocator(c.locator);
|
|
926
|
+
code.style.cssText = "word-break:break-all;";
|
|
927
|
+
const score = doc.createElement("span");
|
|
928
|
+
score.textContent = String(c.score);
|
|
929
|
+
score.style.cssText = "color:#c4b5fd;flex-shrink:0;font-variant-numeric:tabular-nums;";
|
|
930
|
+
btn.appendChild(code);
|
|
931
|
+
btn.appendChild(score);
|
|
932
|
+
btn.addEventListener("click", (e) => {
|
|
933
|
+
e.preventDefault();
|
|
934
|
+
e.stopImmediatePropagation();
|
|
935
|
+
done(i);
|
|
936
|
+
});
|
|
937
|
+
panel.appendChild(btn);
|
|
938
|
+
});
|
|
939
|
+
const skip = doc.createElement("button");
|
|
940
|
+
skip.style.cssText = "background:none;border:none;color:#9ca3af;cursor:pointer;padding:6px 0 0;font:12px system-ui,sans-serif;";
|
|
941
|
+
skip.textContent = "Skip \u2014 keep the failure as-is (Esc)";
|
|
942
|
+
skip.addEventListener("click", (e) => {
|
|
943
|
+
e.preventDefault();
|
|
944
|
+
e.stopImmediatePropagation();
|
|
945
|
+
done(-1);
|
|
946
|
+
});
|
|
947
|
+
panel.appendChild(skip);
|
|
948
|
+
doc.addEventListener("keydown", onKey, true);
|
|
949
|
+
wrap.appendChild(panel);
|
|
950
|
+
doc.body.appendChild(wrap);
|
|
694
951
|
}
|
|
695
952
|
|
|
696
|
-
// src/
|
|
697
|
-
var
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
953
|
+
// ../core/src/locator-generation.ts
|
|
954
|
+
var TAG_TO_ROLE = {
|
|
955
|
+
a: "link",
|
|
956
|
+
button: "button",
|
|
957
|
+
nav: "navigation",
|
|
958
|
+
main: "main",
|
|
959
|
+
article: "article",
|
|
960
|
+
section: "region",
|
|
961
|
+
form: "form",
|
|
962
|
+
img: "img",
|
|
963
|
+
figure: "figure",
|
|
964
|
+
figcaption: "caption",
|
|
965
|
+
blockquote: "blockquote",
|
|
966
|
+
table: "table",
|
|
967
|
+
caption: "caption",
|
|
968
|
+
thead: "rowgroup",
|
|
969
|
+
tbody: "rowgroup",
|
|
970
|
+
tfoot: "rowgroup",
|
|
971
|
+
tr: "row",
|
|
972
|
+
td: "cell",
|
|
973
|
+
ul: "list",
|
|
974
|
+
ol: "list",
|
|
975
|
+
li: "listitem",
|
|
976
|
+
dialog: "dialog",
|
|
977
|
+
output: "status",
|
|
978
|
+
progress: "progressbar",
|
|
979
|
+
meter: "meter",
|
|
980
|
+
textarea: "textbox",
|
|
981
|
+
h1: "heading",
|
|
982
|
+
h2: "heading",
|
|
983
|
+
h3: "heading",
|
|
984
|
+
h4: "heading",
|
|
985
|
+
h5: "heading",
|
|
986
|
+
h6: "heading",
|
|
987
|
+
details: "group",
|
|
988
|
+
summary: "button",
|
|
989
|
+
search: "search"
|
|
990
|
+
};
|
|
991
|
+
var INPUT_TYPE_TO_ROLE = {
|
|
992
|
+
button: "button",
|
|
993
|
+
submit: "button",
|
|
994
|
+
reset: "button",
|
|
995
|
+
image: "button",
|
|
996
|
+
checkbox: "checkbox",
|
|
997
|
+
radio: "radio",
|
|
998
|
+
range: "slider",
|
|
999
|
+
search: "searchbox",
|
|
1000
|
+
number: "spinbutton",
|
|
1001
|
+
text: "textbox",
|
|
1002
|
+
email: "textbox",
|
|
1003
|
+
tel: "textbox",
|
|
1004
|
+
url: "textbox",
|
|
1005
|
+
password: "textbox"
|
|
1006
|
+
};
|
|
1007
|
+
var CAPTURED_ATTRIBUTES = [
|
|
1008
|
+
"id",
|
|
1009
|
+
"class",
|
|
1010
|
+
"name",
|
|
1011
|
+
"data-testid",
|
|
1012
|
+
"placeholder",
|
|
1013
|
+
"alt",
|
|
1014
|
+
"title",
|
|
1015
|
+
"aria-label",
|
|
1016
|
+
"aria-level",
|
|
1017
|
+
"role",
|
|
1018
|
+
"type",
|
|
1019
|
+
"href",
|
|
1020
|
+
"multiple"
|
|
1021
|
+
];
|
|
1022
|
+
function resolveAriaRole(attrs) {
|
|
1023
|
+
const explicit = attrs.attributes["role"];
|
|
1024
|
+
if (explicit) return explicit;
|
|
1025
|
+
const tag = attrs.tagName;
|
|
1026
|
+
if (!tag) return null;
|
|
1027
|
+
if (tag === "input") {
|
|
1028
|
+
const type = (attrs.attributes["type"] ?? "text").toLowerCase();
|
|
1029
|
+
return INPUT_TYPE_TO_ROLE[type] ?? "textbox";
|
|
707
1030
|
}
|
|
708
|
-
|
|
1031
|
+
if (tag === "select") {
|
|
1032
|
+
return attrs.attributes["multiple"] != null ? "listbox" : "combobox";
|
|
1033
|
+
}
|
|
1034
|
+
if (tag === "a") {
|
|
1035
|
+
return attrs.attributes["href"] != null ? "link" : null;
|
|
1036
|
+
}
|
|
1037
|
+
return TAG_TO_ROLE[tag] ?? null;
|
|
1038
|
+
}
|
|
1039
|
+
function headingLevel(attrs, role) {
|
|
1040
|
+
if (role !== "heading") return null;
|
|
1041
|
+
const tagMatch = attrs.tagName.match(/^h([1-6])$/);
|
|
1042
|
+
if (tagMatch) return Number(tagMatch[1]);
|
|
1043
|
+
const ariaLevel = attrs.attributes["aria-level"];
|
|
1044
|
+
if (ariaLevel && /^\d+$/.test(ariaLevel)) return Number(ariaLevel);
|
|
1045
|
+
return null;
|
|
709
1046
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
|
|
1047
|
+
var attr = (a, key) => a.attributes[key] || null;
|
|
1048
|
+
var esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
1049
|
+
var escCssAttrValue = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1050
|
+
var isCssSafeId = (id) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(id);
|
|
1051
|
+
function classifyCssStability(className) {
|
|
1052
|
+
if (/[a-f0-9]{8,}/i.test(className)) return 10;
|
|
1053
|
+
if (/^(?:css|sc|emotion|styled)-/i.test(className)) return 15;
|
|
1054
|
+
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(
|
|
1055
|
+
className
|
|
1056
|
+
))
|
|
1057
|
+
return 25;
|
|
1058
|
+
if (/__/.test(className) || /--/.test(className)) return 35;
|
|
1059
|
+
if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
|
|
1060
|
+
if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
|
|
1061
|
+
if (className.includes("_")) return 15;
|
|
1062
|
+
if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
|
|
1063
|
+
return 15;
|
|
717
1064
|
}
|
|
718
|
-
function
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
else if (raw === "false") obj[key] = false;
|
|
727
|
-
else if (/^-?\d+$/.test(raw)) obj[key] = Number(raw);
|
|
728
|
-
else obj[key] = raw.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
1065
|
+
function isAutoGenerated(value) {
|
|
1066
|
+
if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
|
|
1067
|
+
if (/^[a-f0-9]{8,}$/i.test(value)) return true;
|
|
1068
|
+
if (/^[a-z]+-\d{4,}$/.test(value)) return true;
|
|
1069
|
+
if (/^(tab|panel|input|select|option|dialog|modal|popup|tooltip|menu|listbox|combobox|checkbox|radio|textarea|button|field|label|accordion|collapse|step|slider|toggle|switch|dropdown|overlay|portal|layer)-\d+$/i.test(
|
|
1070
|
+
value
|
|
1071
|
+
)) {
|
|
1072
|
+
return true;
|
|
729
1073
|
}
|
|
730
|
-
return
|
|
1074
|
+
if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
|
|
1075
|
+
if (value.startsWith("ng-")) return true;
|
|
1076
|
+
if (/^(radix-|headlessui-|mui-|mantine-|chakra-)/i.test(value)) return true;
|
|
1077
|
+
if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
|
|
1078
|
+
return false;
|
|
731
1079
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
}
|
|
741
|
-
if (c === "'" || c === '"') {
|
|
742
|
-
const end = endOfString(inner, i);
|
|
743
|
-
args.push(inner.slice(i + 1, end).replace(/\\(.)/g, "$1"));
|
|
744
|
-
i = end + 1;
|
|
745
|
-
continue;
|
|
1080
|
+
var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
|
|
1081
|
+
function generateAlternatives(attrs) {
|
|
1082
|
+
const alts = [];
|
|
1083
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1084
|
+
const add = (loc) => {
|
|
1085
|
+
if (!seen.has(loc.locator)) {
|
|
1086
|
+
seen.add(loc.locator);
|
|
1087
|
+
alts.push(loc);
|
|
746
1088
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1089
|
+
};
|
|
1090
|
+
const { accessibleName } = attrs;
|
|
1091
|
+
const tag = attrs.tagName;
|
|
1092
|
+
const role = resolveAriaRole(attrs);
|
|
1093
|
+
const counts = attrs.selectorCounts;
|
|
1094
|
+
const isUnique = (n) => n == null || n <= 1;
|
|
1095
|
+
const ambiguityPenalty = (n) => n != null && n > 1 ? 45 : 0;
|
|
1096
|
+
const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
|
|
1097
|
+
const testId = attr(attrs, "data-testid");
|
|
1098
|
+
if (testId && isUnique(counts?.testId)) {
|
|
1099
|
+
add({
|
|
1100
|
+
locator: `getByTestId('${esc(testId)}')`,
|
|
1101
|
+
method: "getByTestId",
|
|
1102
|
+
args: { testId },
|
|
1103
|
+
score: 100
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
const level = headingLevel(attrs, role);
|
|
1107
|
+
const levelPart = level != null ? `, level: ${level}` : "";
|
|
1108
|
+
const withLevel = (base) => level != null ? { ...base, level } : base;
|
|
1109
|
+
if (role && accessibleName) {
|
|
1110
|
+
add({
|
|
1111
|
+
locator: `getByRole('${role}', { name: '${esc(accessibleName)}'${levelPart} })`,
|
|
1112
|
+
method: "getByRole",
|
|
1113
|
+
args: withLevel({ role, name: accessibleName }),
|
|
1114
|
+
score: 90 - ambiguityPenalty(counts?.roleName)
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
const ariaLabel = attr(attrs, "aria-label");
|
|
1118
|
+
if (role && ariaLabel && ariaLabel !== accessibleName) {
|
|
1119
|
+
add({
|
|
1120
|
+
locator: `getByRole('${role}', { name: '${esc(ariaLabel)}'${levelPart} })`,
|
|
1121
|
+
method: "getByRole",
|
|
1122
|
+
args: withLevel({ role, name: ariaLabel }),
|
|
1123
|
+
score: 85 - ambiguityPenalty(counts?.roleName)
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
if (accessibleName && ["input", "select", "textarea"].includes(tag)) {
|
|
1127
|
+
const label = attr(attrs, "aria-label");
|
|
1128
|
+
const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
|
|
1129
|
+
if (labelBacked) {
|
|
1130
|
+
add({
|
|
1131
|
+
locator: `getByLabel('${esc(accessibleName)}')`,
|
|
1132
|
+
method: "getByLabel",
|
|
1133
|
+
args: { label: accessibleName },
|
|
1134
|
+
score: 85
|
|
1135
|
+
});
|
|
752
1136
|
}
|
|
753
|
-
i++;
|
|
754
1137
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1138
|
+
const placeholder = attr(attrs, "placeholder");
|
|
1139
|
+
if (placeholder) {
|
|
1140
|
+
add({
|
|
1141
|
+
locator: `getByPlaceholder('${esc(placeholder)}')`,
|
|
1142
|
+
method: "getByPlaceholder",
|
|
1143
|
+
args: { placeholder },
|
|
1144
|
+
score: 80 - ambiguityPenalty(counts?.placeholder)
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
if (text && text.length < 80) {
|
|
1148
|
+
add({
|
|
1149
|
+
locator: `getByText('${esc(text)}')`,
|
|
1150
|
+
method: "getByText",
|
|
1151
|
+
args: { text },
|
|
1152
|
+
score: 75 - ambiguityPenalty(counts?.text)
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
const id = attr(attrs, "id");
|
|
1156
|
+
if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
1157
|
+
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
1158
|
+
add({
|
|
1159
|
+
locator: `locator('${esc(selector)}')`,
|
|
1160
|
+
method: "locator",
|
|
1161
|
+
args: { selector },
|
|
1162
|
+
score: 65
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
const name = attr(attrs, "name");
|
|
1166
|
+
if (name && isUnique(counts?.name)) {
|
|
1167
|
+
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
1168
|
+
add({
|
|
1169
|
+
locator: `locator('${esc(selector)}')`,
|
|
1170
|
+
method: "locator",
|
|
1171
|
+
args: { selector },
|
|
1172
|
+
score: 60
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
const alt = attr(attrs, "alt");
|
|
1176
|
+
if (alt) {
|
|
1177
|
+
add({
|
|
1178
|
+
locator: `getByAltText('${esc(alt)}')`,
|
|
1179
|
+
method: "getByAltText",
|
|
1180
|
+
args: { text: alt },
|
|
1181
|
+
score: 60 - ambiguityPenalty(counts?.alt)
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
const title = attr(attrs, "title");
|
|
1185
|
+
if (title) {
|
|
1186
|
+
add({
|
|
1187
|
+
locator: `getByTitle('${esc(title)}')`,
|
|
1188
|
+
method: "getByTitle",
|
|
1189
|
+
args: { title },
|
|
1190
|
+
score: 50 - ambiguityPenalty(counts?.title)
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
const hasOwnTestId = !!(testId && isUnique(counts?.testId));
|
|
1194
|
+
const addAnchoredChains = (leaf, scopedCount, scores) => {
|
|
1195
|
+
let testIdAnchorDone = false;
|
|
1196
|
+
let idAnchorDone = false;
|
|
1197
|
+
let roleAnchorDone = false;
|
|
1198
|
+
let dataAnchorDone = false;
|
|
1199
|
+
let filterAnchorDone = false;
|
|
1200
|
+
for (const anc of attrs.ancestors ?? []) {
|
|
1201
|
+
if (scopedCount(anc) !== 1) continue;
|
|
1202
|
+
if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
|
|
1203
|
+
testIdAnchorDone = true;
|
|
1204
|
+
add({
|
|
1205
|
+
locator: `getByTestId('${esc(anc.testId)}').${leaf.expr}`,
|
|
1206
|
+
method: leaf.method,
|
|
1207
|
+
args: { ...leaf.args, anchorTestId: anc.testId },
|
|
1208
|
+
score: scores.testId
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
1212
|
+
idAnchorDone = true;
|
|
1213
|
+
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
1214
|
+
add({
|
|
1215
|
+
locator: `locator('${esc(anchorSelector)}').${leaf.expr}`,
|
|
1216
|
+
method: leaf.method,
|
|
1217
|
+
args: { ...leaf.args, anchorSelector },
|
|
1218
|
+
score: scores.id
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
if (!dataAnchorDone && anc.dataAttr && anc.dataAttrCount === 1) {
|
|
1222
|
+
dataAnchorDone = true;
|
|
1223
|
+
const { name: name2, value } = anc.dataAttr;
|
|
1224
|
+
const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
|
|
1225
|
+
add({
|
|
1226
|
+
locator: `locator('${esc(anchorSelector)}').${leaf.expr}`,
|
|
1227
|
+
method: leaf.method,
|
|
1228
|
+
args: { ...leaf.args, anchorSelector },
|
|
1229
|
+
score: TEST_DATA_ATTRS.has(name2) ? scores.testData : scores.data
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
|
|
1233
|
+
if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
|
|
1234
|
+
roleAnchorDone = true;
|
|
1235
|
+
add({
|
|
1236
|
+
locator: `getByRole('${esc(ancestorRole)}').${leaf.expr}`,
|
|
1237
|
+
method: leaf.method,
|
|
1238
|
+
args: { ...leaf.args, anchorRole: ancestorRole },
|
|
1239
|
+
score: scores.role
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
|
|
1243
|
+
filterAnchorDone = true;
|
|
1244
|
+
add({
|
|
1245
|
+
locator: `getByRole('${esc(ancestorRole)}').filter({ hasText: '${esc(anc.filterText)}' }).${leaf.expr}`,
|
|
1246
|
+
method: leaf.method,
|
|
1247
|
+
args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
|
|
1248
|
+
score: scores.filter
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
765
1251
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1252
|
+
};
|
|
1253
|
+
if (role && !hasOwnTestId) {
|
|
1254
|
+
const rolePart = level != null ? `'${role}', { level: ${level} }` : `'${role}'`;
|
|
1255
|
+
const leafArgs = withLevel({ role });
|
|
1256
|
+
addAnchoredChains(
|
|
1257
|
+
{ expr: `getByRole(${rolePart})`, method: "getByRole", args: leafArgs, role },
|
|
1258
|
+
(anc) => anc.scopedRoleCount,
|
|
1259
|
+
{ testId: 72, testData: 70, id: 64, data: 60, role: 55, filter: 53 }
|
|
1260
|
+
);
|
|
1261
|
+
const pos = attrs.rolePosition;
|
|
1262
|
+
if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
|
|
1263
|
+
add({
|
|
1264
|
+
locator: `getByRole(${rolePart})`,
|
|
1265
|
+
method: "getByRole",
|
|
1266
|
+
args: leafArgs,
|
|
1267
|
+
score: 58
|
|
1268
|
+
});
|
|
770
1269
|
}
|
|
1270
|
+
} else if (!role && text && text.length < 80 && !hasOwnTestId) {
|
|
1271
|
+
addAnchoredChains(
|
|
1272
|
+
{ expr: `getByText('${esc(text)}')`, method: "getByText", args: { text }, role: null },
|
|
1273
|
+
(anc) => anc.scopedTextCount,
|
|
1274
|
+
{ testId: 68, testData: 66, id: 60, data: 56, role: 51, filter: 49 }
|
|
1275
|
+
);
|
|
771
1276
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const text = `${err.message ?? ""}
|
|
785
|
-
${err.stack ?? ""}`.replace(ANSI_RE, "");
|
|
786
|
-
const line = /^\s*Locator:\s*(.+)$/m.exec(text);
|
|
787
|
-
if (!line) continue;
|
|
788
|
-
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
789
|
-
if (!parsed) continue;
|
|
790
|
-
const loc = err.location;
|
|
791
|
-
const location = loc ? `${path2.relative(process.cwd(), loc.file).split(path2.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
792
|
-
return { method: parsed.method, args: parsed.args, location };
|
|
1277
|
+
const clsStr = attr(attrs, "class");
|
|
1278
|
+
if (clsStr) {
|
|
1279
|
+
const countsClasses = counts?.classes;
|
|
1280
|
+
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);
|
|
1281
|
+
for (const { cls, score } of classes) {
|
|
1282
|
+
add({
|
|
1283
|
+
locator: `locator('.${esc(cls)}')`,
|
|
1284
|
+
method: "locator",
|
|
1285
|
+
args: { selector: `.${cls}` },
|
|
1286
|
+
score
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
793
1289
|
}
|
|
1290
|
+
return alts.sort((a, b) => b.score - a.score);
|
|
1291
|
+
}
|
|
1292
|
+
function approximateAccessibleName(attrs) {
|
|
1293
|
+
const a = attrs.attributes;
|
|
1294
|
+
const ariaLabel = a["aria-label"];
|
|
1295
|
+
if (ariaLabel) return ariaLabel;
|
|
1296
|
+
if (attrs.textContent) return attrs.textContent;
|
|
1297
|
+
const title = a["title"];
|
|
1298
|
+
if (title) return title;
|
|
1299
|
+
const placeholder = a["placeholder"];
|
|
1300
|
+
if (placeholder) return placeholder;
|
|
794
1301
|
return null;
|
|
795
1302
|
}
|
|
1303
|
+
|
|
1304
|
+
// ../picker-dom/src/anchor-alternatives.ts
|
|
796
1305
|
var escStr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
797
1306
|
var cssIdSelector = (id) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id) ? `#${id}` : `[id="${id.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"]`;
|
|
798
1307
|
var ANCHOR_KIND_SCORES = { testid: 80, id: 74, labeledRole: 68, role: 62 };
|
|
@@ -867,511 +1376,455 @@ function mergeCandidates(base, extra) {
|
|
|
867
1376
|
}
|
|
868
1377
|
return merged.sort((a, b) => b.score - a.score);
|
|
869
1378
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1379
|
+
|
|
1380
|
+
// src/internal/capture/locator-healing.ts
|
|
1381
|
+
var path = __toESM(require("path"));
|
|
1382
|
+
|
|
1383
|
+
// ../core/src/locator-fingerprint.ts
|
|
1384
|
+
var PRESENT_SIMILARITY = 0.8;
|
|
1385
|
+
var MATCH_SIMILARITY = 0.2;
|
|
1386
|
+
function parseAriaCandidates(ariaSnapshot) {
|
|
1387
|
+
if (!ariaSnapshot) return [];
|
|
1388
|
+
const out = [];
|
|
1389
|
+
for (const line of ariaSnapshot.split("\n")) {
|
|
1390
|
+
const m = line.match(/^\s*-\s+([a-z]+)(?:\s+"((?:[^"\\]|\\.)*)")?/i);
|
|
1391
|
+
if (!m) continue;
|
|
1392
|
+
const role = m[1];
|
|
1393
|
+
const name = m[2] != null ? m[2].replace(/\\(.)/g, "$1") : null;
|
|
1394
|
+
if (!name && (role === "generic" || role === "group" || role === "list" || role === "paragraph")) continue;
|
|
1395
|
+
const levelMatch = line.slice(m[0].length).match(/\[level=(\d+)\]/);
|
|
1396
|
+
const level = levelMatch ? Number(levelMatch[1]) : null;
|
|
1397
|
+
out.push({ role, name, level });
|
|
876
1398
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
if (ariaLabel) return `getByLabel('${escJs(ariaLabel)}')`;
|
|
917
|
-
const placeholder = el.getAttribute && el.getAttribute("placeholder");
|
|
918
|
-
if (placeholder) return `getByPlaceholder('${escJs(placeholder)}')`;
|
|
919
|
-
const alt = el.getAttribute && el.getAttribute("alt");
|
|
920
|
-
if (alt) return `getByAltText('${escJs(alt)}')`;
|
|
921
|
-
const titleAttr = el.getAttribute && el.getAttribute("title");
|
|
922
|
-
if (titleAttr) return `getByTitle('${escJs(titleAttr)}')`;
|
|
923
|
-
if (el.id) return `locator('#${escJs(el.id)}')`;
|
|
924
|
-
const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
|
|
925
|
-
return cls ? `locator('.${escJs(cls)}')` : tag;
|
|
926
|
-
};
|
|
927
|
-
const buildChain = (raw) => {
|
|
928
|
-
const chain2 = [];
|
|
929
|
-
let node = raw;
|
|
930
|
-
while (node && chain2.length < 15) {
|
|
931
|
-
const tag = (node.tagName || "").toLowerCase();
|
|
932
|
-
if (tag === "body" || tag === "html") break;
|
|
933
|
-
chain2.push(node);
|
|
934
|
-
node = node.parentElement;
|
|
935
|
-
}
|
|
936
|
-
return chain2.length ? chain2 : [raw];
|
|
937
|
-
};
|
|
938
|
-
const ACTIONABLE_TAGS = ["button", "a", "input", "select", "textarea", "summary", "option"];
|
|
939
|
-
const snapIndex = (chain2) => {
|
|
940
|
-
for (let i = 0; i < Math.min(chain2.length, 4); i++) {
|
|
941
|
-
const el = chain2[i];
|
|
942
|
-
const tag = (el.tagName || "").toLowerCase();
|
|
943
|
-
if (ACTIONABLE_TAGS.includes(tag)) return i;
|
|
944
|
-
if (el.getAttribute && (el.getAttribute("role") || el.getAttribute("data-testid"))) return i;
|
|
945
|
-
}
|
|
946
|
-
return 0;
|
|
947
|
-
};
|
|
948
|
-
let chain = [];
|
|
949
|
-
let idx = 0;
|
|
950
|
-
let lastRaw = null;
|
|
951
|
-
const current = () => chain[idx] ?? null;
|
|
952
|
-
const refresh = () => {
|
|
953
|
-
const el = current();
|
|
954
|
-
if (!el) {
|
|
955
|
-
highlight.style.display = "none";
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
const r = el.getBoundingClientRect();
|
|
959
|
-
highlight.style.display = "block";
|
|
960
|
-
highlight.style.left = r.left + "px";
|
|
961
|
-
highlight.style.top = r.top + "px";
|
|
962
|
-
highlight.style.width = r.width + "px";
|
|
963
|
-
highlight.style.height = r.height + "px";
|
|
964
|
-
foot.textContent = `${describe(el)} \u2014 click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip`;
|
|
965
|
-
};
|
|
966
|
-
const stop = (e) => {
|
|
967
|
-
e.preventDefault();
|
|
968
|
-
e.stopImmediatePropagation();
|
|
969
|
-
};
|
|
970
|
-
const isOwn = (el) => el === banner || el === highlight || banner.contains && banner.contains(el);
|
|
971
|
-
let bannerDocked = "top";
|
|
972
|
-
const dockBanner = (side) => {
|
|
973
|
-
if (bannerDocked === side) return;
|
|
974
|
-
bannerDocked = side;
|
|
975
|
-
if (side === "bottom") {
|
|
976
|
-
banner.style.top = "auto";
|
|
977
|
-
banner.style.bottom = "12px";
|
|
978
|
-
} else {
|
|
979
|
-
banner.style.top = "12px";
|
|
980
|
-
banner.style.bottom = "auto";
|
|
981
|
-
}
|
|
982
|
-
};
|
|
983
|
-
const onMove = (e) => {
|
|
984
|
-
const raw = e.target;
|
|
985
|
-
if (!raw || isOwn(raw)) {
|
|
986
|
-
highlight.style.display = "none";
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
if (raw !== lastRaw) {
|
|
990
|
-
lastRaw = raw;
|
|
991
|
-
chain = buildChain(raw);
|
|
992
|
-
idx = snapIndex(chain);
|
|
993
|
-
}
|
|
994
|
-
refresh();
|
|
995
|
-
const el = current();
|
|
996
|
-
if (el) {
|
|
997
|
-
const r = el.getBoundingClientRect();
|
|
998
|
-
const br = banner.getBoundingClientRect();
|
|
999
|
-
const margin = 8;
|
|
1000
|
-
if (r.left < br.right + margin && r.right > br.left - margin && r.top < br.bottom + margin && r.bottom > br.top - margin) {
|
|
1001
|
-
dockBanner(bannerDocked === "top" ? "bottom" : "top");
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
};
|
|
1005
|
-
const onClick = (e) => {
|
|
1006
|
-
stop(e);
|
|
1007
|
-
const el = current();
|
|
1008
|
-
if (!el || isOwn(e.target)) return;
|
|
1009
|
-
g.__piwiPickedElement = el;
|
|
1010
|
-
g.__piwiPickState = "picked";
|
|
1011
|
-
removeListeners();
|
|
1012
|
-
highlight.style.display = "none";
|
|
1013
|
-
foot.textContent = "Analyzing element\u2026";
|
|
1014
|
-
};
|
|
1015
|
-
const onKey = (e) => {
|
|
1016
|
-
if (e.key === "Escape") {
|
|
1017
|
-
stop(e);
|
|
1018
|
-
g.__piwiPickState = "skipped";
|
|
1019
|
-
cleanup();
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1022
|
-
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
1023
|
-
stop(e);
|
|
1024
|
-
if (e.key === "ArrowUp") idx = Math.min(idx + 1, chain.length - 1);
|
|
1025
|
-
else idx = Math.max(idx - 1, 0);
|
|
1026
|
-
refresh();
|
|
1399
|
+
return out;
|
|
1400
|
+
}
|
|
1401
|
+
function textSimilarity(a, b) {
|
|
1402
|
+
const tok = (s) => new Set(
|
|
1403
|
+
(s ?? "").toLowerCase().split(/[^a-z0-9]+/i).filter(Boolean)
|
|
1404
|
+
);
|
|
1405
|
+
const sa = tok(a);
|
|
1406
|
+
const sb = tok(b);
|
|
1407
|
+
if (sa.size === 0 && sb.size === 0) return 1;
|
|
1408
|
+
if (sa.size === 0 || sb.size === 0) return 0;
|
|
1409
|
+
let common = 0;
|
|
1410
|
+
for (const t of sa) if (sb.has(t)) common++;
|
|
1411
|
+
return 2 * common / (sa.size + sb.size);
|
|
1412
|
+
}
|
|
1413
|
+
function fingerprintPresent(fp, candidates) {
|
|
1414
|
+
if (!fp.name) return false;
|
|
1415
|
+
return candidates.some(
|
|
1416
|
+
(c) => (!fp.role || c.role === fp.role) && textSimilarity(c.name, fp.name) >= PRESENT_SIMILARITY
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
function matchRenamedElement(fp, candidates) {
|
|
1420
|
+
if (candidates.length === 0) return null;
|
|
1421
|
+
const sameRole = fp.role ? candidates.filter((c) => c.role === fp.role) : candidates;
|
|
1422
|
+
if (sameRole.length === 0) return null;
|
|
1423
|
+
let pool = sameRole;
|
|
1424
|
+
if (fp.level != null) {
|
|
1425
|
+
const sameLevel = sameRole.filter((c) => c.level === fp.level);
|
|
1426
|
+
if (sameLevel.length > 0) pool = sameLevel;
|
|
1427
|
+
}
|
|
1428
|
+
if (pool.length === 1) {
|
|
1429
|
+
return { candidate: pool[0], confidence: 0.7 };
|
|
1430
|
+
}
|
|
1431
|
+
let best = null;
|
|
1432
|
+
let bestScore = -1;
|
|
1433
|
+
for (const c of pool) {
|
|
1434
|
+
const s = textSimilarity(c.name, fp.name);
|
|
1435
|
+
if (s > bestScore) {
|
|
1436
|
+
bestScore = s;
|
|
1437
|
+
best = c;
|
|
1027
1438
|
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1439
|
+
}
|
|
1440
|
+
if (best && bestScore >= MATCH_SIMILARITY) return { candidate: best, confidence: bestScore };
|
|
1441
|
+
const pos = fp.rolePosition;
|
|
1442
|
+
if (pos && fp.role && pos.role === fp.role && sameRole.length >= 2 && sameRole.length === pos.count) {
|
|
1443
|
+
const byIndex = sameRole[pos.index];
|
|
1444
|
+
if (byIndex) return { candidate: byIndex, confidence: 0.5 };
|
|
1445
|
+
}
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// ../core/src/locator-methods.ts
|
|
1450
|
+
var LOCATOR_BUILDER_METHODS = [
|
|
1451
|
+
"getByRole",
|
|
1452
|
+
"getByTestId",
|
|
1453
|
+
"getByText",
|
|
1454
|
+
"getByLabel",
|
|
1455
|
+
"getByPlaceholder",
|
|
1456
|
+
"getByAltText",
|
|
1457
|
+
"getByTitle",
|
|
1458
|
+
"locator"
|
|
1459
|
+
];
|
|
1460
|
+
|
|
1461
|
+
// src/internal/capture/locator-healing.ts
|
|
1462
|
+
function dedupeSnapshotsByLocation(snaps) {
|
|
1463
|
+
const lastWithElement = /* @__PURE__ */ new Map();
|
|
1464
|
+
const lastAny = /* @__PURE__ */ new Map();
|
|
1465
|
+
snaps.forEach((s, i) => {
|
|
1466
|
+
if (!s.location) return;
|
|
1467
|
+
lastAny.set(s.location, i);
|
|
1468
|
+
if (s.element) lastWithElement.set(s.location, i);
|
|
1469
|
+
});
|
|
1470
|
+
return snaps.filter((s, i) => {
|
|
1471
|
+
if (!s.location) return true;
|
|
1472
|
+
return (lastWithElement.get(s.location) ?? lastAny.get(s.location)) === i;
|
|
1473
|
+
});
|
|
1046
1474
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1475
|
+
var LOCATOR_METHODS = [...LOCATOR_BUILDER_METHODS];
|
|
1476
|
+
var CHAIN_METHODS = [
|
|
1477
|
+
"first",
|
|
1478
|
+
"nth",
|
|
1479
|
+
"last",
|
|
1480
|
+
"filter",
|
|
1481
|
+
"and",
|
|
1482
|
+
"or",
|
|
1483
|
+
"locator",
|
|
1484
|
+
"getByRole",
|
|
1485
|
+
"getByTestId",
|
|
1486
|
+
"getByText",
|
|
1487
|
+
"getByLabel",
|
|
1488
|
+
"getByPlaceholder",
|
|
1489
|
+
"getByAltText",
|
|
1490
|
+
"getByTitle"
|
|
1491
|
+
];
|
|
1492
|
+
var ACTION_METHODS = [
|
|
1493
|
+
"click",
|
|
1494
|
+
"fill",
|
|
1495
|
+
"check",
|
|
1496
|
+
"uncheck",
|
|
1497
|
+
"selectOption",
|
|
1498
|
+
"dblclick",
|
|
1499
|
+
"tap",
|
|
1500
|
+
"hover",
|
|
1501
|
+
"press",
|
|
1502
|
+
"type",
|
|
1503
|
+
"pressSequentially",
|
|
1504
|
+
"clear",
|
|
1505
|
+
"setInputFiles",
|
|
1506
|
+
"dragTo",
|
|
1507
|
+
"focus",
|
|
1508
|
+
"blur",
|
|
1509
|
+
"scrollIntoViewIfNeeded",
|
|
1510
|
+
"dispatchEvent",
|
|
1511
|
+
"selectText",
|
|
1512
|
+
// Not an action, but a successful waitFor proves the element resolved — the
|
|
1513
|
+
// closest capture hook available for assertion-style usage of a locator.
|
|
1514
|
+
"waitFor"
|
|
1515
|
+
];
|
|
1516
|
+
var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
|
|
1517
|
+
var EXPECT_METHOD = "_expect";
|
|
1518
|
+
var EXPECT_CAPTURE_EXPRESSIONS = /* @__PURE__ */ new Set([
|
|
1519
|
+
"to.be.attached",
|
|
1520
|
+
"to.be.checked",
|
|
1521
|
+
"to.be.disabled",
|
|
1522
|
+
"to.be.editable",
|
|
1523
|
+
"to.be.empty",
|
|
1524
|
+
"to.be.enabled",
|
|
1525
|
+
"to.be.focused",
|
|
1526
|
+
"to.be.in.viewport",
|
|
1527
|
+
"to.be.readonly",
|
|
1528
|
+
"to.be.visible",
|
|
1529
|
+
"to.contain.class",
|
|
1530
|
+
"to.contain.text",
|
|
1531
|
+
"to.have.accessible.description",
|
|
1532
|
+
"to.have.accessible.error.message",
|
|
1533
|
+
"to.have.accessible.name",
|
|
1534
|
+
"to.have.attribute",
|
|
1535
|
+
"to.have.attribute.value",
|
|
1536
|
+
"to.have.class",
|
|
1537
|
+
"to.have.css",
|
|
1538
|
+
"to.have.id",
|
|
1539
|
+
"to.have.js.property",
|
|
1540
|
+
"to.have.role",
|
|
1541
|
+
"to.have.text",
|
|
1542
|
+
"to.have.value",
|
|
1543
|
+
"to.match.aria"
|
|
1544
|
+
]);
|
|
1545
|
+
function extractAccessibleName(ariaSnapshot) {
|
|
1546
|
+
if (!ariaSnapshot) return null;
|
|
1547
|
+
const match = ariaSnapshot.match(/- \w+ "([^"]+)"/);
|
|
1548
|
+
if (match) return match[1];
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
|
|
1552
|
+
"getByText",
|
|
1553
|
+
"getByRole",
|
|
1554
|
+
"getByLabel",
|
|
1555
|
+
"getByPlaceholder",
|
|
1556
|
+
"getByTitle",
|
|
1557
|
+
"getByAltText"
|
|
1558
|
+
]);
|
|
1559
|
+
var escAttr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
1560
|
+
var SUGG_TEXT_ROLES = /* @__PURE__ */ new Set([
|
|
1561
|
+
"button",
|
|
1562
|
+
"link",
|
|
1563
|
+
"heading",
|
|
1564
|
+
"menuitem",
|
|
1565
|
+
"tab",
|
|
1566
|
+
"option",
|
|
1567
|
+
"cell",
|
|
1568
|
+
"columnheader",
|
|
1569
|
+
"rowheader",
|
|
1570
|
+
"gridcell",
|
|
1571
|
+
"treeitem",
|
|
1572
|
+
"listitem",
|
|
1573
|
+
"checkbox",
|
|
1574
|
+
"radio",
|
|
1575
|
+
"switch"
|
|
1576
|
+
]);
|
|
1577
|
+
var SUGG_FIELD_ROLES = /* @__PURE__ */ new Set(["textbox", "combobox", "searchbox", "spinbutton", "slider"]);
|
|
1578
|
+
function failedNameAndRole(failed) {
|
|
1579
|
+
if (failed.method === "getByRole") {
|
|
1580
|
+
const role = typeof failed.args[0] === "string" ? failed.args[0] : null;
|
|
1581
|
+
const opts = failed.args[1];
|
|
1582
|
+
const name = opts && typeof opts.name === "string" ? opts.name : null;
|
|
1583
|
+
const level = opts && typeof opts.level === "number" ? opts.level : null;
|
|
1584
|
+
return { role, name, level };
|
|
1054
1585
|
}
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
if (tag === "select") return n.getAttribute("multiple") != null ? "listbox" : "combobox";
|
|
1063
|
-
if (tag === "a") return n.getAttribute("href") != null ? "link" : null;
|
|
1064
|
-
return tagRoles[tag] ?? null;
|
|
1065
|
-
};
|
|
1066
|
-
const levelOf = (n) => {
|
|
1067
|
-
const m = /^h([1-6])$/.exec((n.tagName || "").toLowerCase());
|
|
1068
|
-
if (m) return Number(m[1]);
|
|
1069
|
-
const al = n.getAttribute && n.getAttribute("aria-level");
|
|
1070
|
-
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
1071
|
-
};
|
|
1072
|
-
const leafMatches = (scope) => {
|
|
1073
|
-
try {
|
|
1074
|
-
if (leafTestId) return scope.querySelectorAll(`[data-testid=${JSON.stringify(leafTestId)}]`).length;
|
|
1075
|
-
const nodes = scope.querySelectorAll(roleSources);
|
|
1076
|
-
if (nodes.length > 2e3) return -1;
|
|
1077
|
-
let matched = 0;
|
|
1078
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1079
|
-
const n = nodes[i];
|
|
1080
|
-
if (roleOf(n) !== leafRole) continue;
|
|
1081
|
-
if (leafLevel != null && levelOf(n) !== leafLevel) continue;
|
|
1082
|
-
matched++;
|
|
1083
|
-
}
|
|
1084
|
-
return matched;
|
|
1085
|
-
} catch {
|
|
1086
|
-
return -1;
|
|
1087
|
-
}
|
|
1088
|
-
};
|
|
1089
|
-
let roleNodes = [];
|
|
1090
|
-
try {
|
|
1091
|
-
const all = doc.querySelectorAll(roleSources);
|
|
1092
|
-
if (all.length <= 4e3) roleNodes = Array.from(all);
|
|
1093
|
-
} catch {
|
|
1094
|
-
roleNodes = [];
|
|
1586
|
+
const first = failed.args.find((a) => typeof a === "string");
|
|
1587
|
+
return { role: null, name: typeof first === "string" ? first : null, level: null };
|
|
1588
|
+
}
|
|
1589
|
+
function renderFailing(failed) {
|
|
1590
|
+
const { role, name } = failedNameAndRole(failed);
|
|
1591
|
+
if (failed.method === "getByRole") {
|
|
1592
|
+
return name ? `getByRole('${escAttr(role ?? "")}', { name: '${escAttr(name)}' })` : `getByRole('${escAttr(role ?? "")}')`;
|
|
1095
1593
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1594
|
+
return `${failed.method}('${escAttr(name ?? "")}')`;
|
|
1595
|
+
}
|
|
1596
|
+
function freshSuggestions(candidate, failedMethod) {
|
|
1597
|
+
const out = [];
|
|
1598
|
+
const role = candidate.role;
|
|
1599
|
+
const name = candidate.name;
|
|
1600
|
+
const push = (s) => {
|
|
1601
|
+
if (!out.includes(s)) out.push(s);
|
|
1102
1602
|
};
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1603
|
+
const levelPart = candidate.level != null ? `, level: ${candidate.level}` : "";
|
|
1604
|
+
const roleLoc = `getByRole('${escAttr(role)}', { name: '${escAttr(name)}'${levelPart} })`;
|
|
1605
|
+
const textLoc = `getByText('${escAttr(name)}')`;
|
|
1606
|
+
const labelLoc = `getByLabel('${escAttr(name)}')`;
|
|
1607
|
+
if (failedMethod === "getByText" && SUGG_TEXT_ROLES.has(role)) push(textLoc);
|
|
1608
|
+
if (failedMethod === "getByLabel" && SUGG_FIELD_ROLES.has(role)) push(labelLoc);
|
|
1609
|
+
push(roleLoc);
|
|
1610
|
+
if (SUGG_TEXT_ROLES.has(role)) push(textLoc);
|
|
1611
|
+
else if (SUGG_FIELD_ROLES.has(role)) push(labelLoc);
|
|
1612
|
+
return out;
|
|
1613
|
+
}
|
|
1614
|
+
function suggestLocatorsFromAria(failed, ariaSnapshot) {
|
|
1615
|
+
if (!ariaSnapshot || !NAME_BASED_METHODS.has(failed.method)) return null;
|
|
1616
|
+
const { role, name, level } = failedNameAndRole(failed);
|
|
1617
|
+
if (!name) return null;
|
|
1618
|
+
const candidates = parseAriaCandidates(ariaSnapshot);
|
|
1619
|
+
if (candidates.length === 0) return null;
|
|
1620
|
+
const fingerprint = { role, name, level };
|
|
1621
|
+
if (fingerprintPresent(fingerprint, candidates)) return null;
|
|
1622
|
+
const best = matchRenamedElement(fingerprint, candidates)?.candidate;
|
|
1623
|
+
if (!best || !best.name) return null;
|
|
1624
|
+
const suggestions = freshSuggestions({ role: best.role, name: best.name, level: best.level }, failed.method);
|
|
1625
|
+
if (suggestions.length === 0) return null;
|
|
1626
|
+
return { failing: renderFailing(failed), suggestions };
|
|
1627
|
+
}
|
|
1628
|
+
function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
1629
|
+
const lines = stack.split("\n");
|
|
1630
|
+
let prevWasCaptureModule = false;
|
|
1631
|
+
let selfFile = null;
|
|
1632
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1633
|
+
const line = lines[i].trim();
|
|
1634
|
+
if (!line.startsWith("at")) continue;
|
|
1635
|
+
const m = line.match(/^at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);
|
|
1636
|
+
if (!m) {
|
|
1637
|
+
prevWasCaptureModule = false;
|
|
1638
|
+
continue;
|
|
1121
1639
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (ariaLabel) info.labeledRoleCount = labeledCount;
|
|
1640
|
+
let file = m[2];
|
|
1641
|
+
if (!file || file.startsWith("node:")) {
|
|
1642
|
+
prevWasCaptureModule = false;
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
file = file.replace(/^file:\/\/\/?/, "");
|
|
1646
|
+
if (!/\.[a-z]+$/i.test(file)) {
|
|
1647
|
+
prevWasCaptureModule = false;
|
|
1648
|
+
continue;
|
|
1132
1649
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
info,
|
|
1138
|
-
hookLabel,
|
|
1139
|
-
selectable: !!(testId || id || role && (ariaLabel || info.roleCount === 1))
|
|
1140
|
-
});
|
|
1141
|
-
node = node.parentElement;
|
|
1142
|
-
}
|
|
1143
|
-
if (rows.length === 0) {
|
|
1144
|
-
g.__piwiAnchorState = "skipped";
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
const outline = doc.createElement("div");
|
|
1148
|
-
outline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #22c55e;border-radius:3px;display:none;`;
|
|
1149
|
-
const pickedOutline = doc.createElement("div");
|
|
1150
|
-
const pr = el.getBoundingClientRect();
|
|
1151
|
-
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;`;
|
|
1152
|
-
const panel = doc.createElement("div");
|
|
1153
|
-
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);`;
|
|
1154
|
-
const title = doc.createElement("div");
|
|
1155
|
-
title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
|
|
1156
|
-
title.textContent = "Scope to stable parents (optional)";
|
|
1157
|
-
const sub = doc.createElement("div");
|
|
1158
|
-
sub.style.cssText = "color:#9ca3af;margin-bottom:10px;";
|
|
1159
|
-
sub.textContent = "Pick one or more parents to anchor the locator to. Hover a row to see the parent.";
|
|
1160
|
-
panel.appendChild(title);
|
|
1161
|
-
panel.appendChild(sub);
|
|
1162
|
-
const selected = /* @__PURE__ */ new Set();
|
|
1163
|
-
const footer = doc.createElement("div");
|
|
1164
|
-
footer.style.cssText = "margin:10px 0;font-weight:600;";
|
|
1165
|
-
const segMatches = (scope, info) => {
|
|
1166
|
-
try {
|
|
1167
|
-
if (info.testId) return Array.from(scope.querySelectorAll(`[data-testid=${JSON.stringify(info.testId)}]`));
|
|
1168
|
-
if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
|
|
1169
|
-
const nodes = Array.from(scope.querySelectorAll(roleSources));
|
|
1170
|
-
if (nodes.length > 2e3) return [];
|
|
1171
|
-
return nodes.filter(
|
|
1172
|
-
(n) => roleOf(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
|
|
1173
|
-
);
|
|
1174
|
-
} catch {
|
|
1175
|
-
return [];
|
|
1650
|
+
if (selfFile === null || file === selfFile) {
|
|
1651
|
+
selfFile ??= file;
|
|
1652
|
+
prevWasCaptureModule = /[\\/]locator-healing\.[a-z]+$/i.test(file);
|
|
1653
|
+
continue;
|
|
1176
1654
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (chosen.length === 0) return -1;
|
|
1181
|
-
let scopes = [doc];
|
|
1182
|
-
for (const row of chosen) {
|
|
1183
|
-
const next = [];
|
|
1184
|
-
for (const s of scopes) next.push(...segMatches(s, row.info));
|
|
1185
|
-
scopes = next.slice(0, 200);
|
|
1186
|
-
if (scopes.length === 0) return 0;
|
|
1655
|
+
if (/[\\/]locator-healing\.[a-z]+$/i.test(file)) {
|
|
1656
|
+
prevWasCaptureModule = true;
|
|
1657
|
+
continue;
|
|
1187
1658
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
if (c > 0) total += c;
|
|
1192
|
-
if (total > 50) return total;
|
|
1659
|
+
if (prevWasCaptureModule && /[\\/](?:capture-)?fixtures\.[a-z]+$/i.test(file)) {
|
|
1660
|
+
prevWasCaptureModule = false;
|
|
1661
|
+
continue;
|
|
1193
1662
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
if (selected.size === 0) {
|
|
1198
|
-
footer.textContent = "No parents selected \u2014 standard alternatives only.";
|
|
1199
|
-
footer.style.color = "#9ca3af";
|
|
1200
|
-
g.__piwiPickChainCount = void 0;
|
|
1201
|
-
return;
|
|
1663
|
+
if (/[\\/]node_modules[\\/]/.test(file)) {
|
|
1664
|
+
prevWasCaptureModule = false;
|
|
1665
|
+
continue;
|
|
1202
1666
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
footer.style.color = "#4ade80";
|
|
1208
|
-
} else {
|
|
1209
|
-
footer.textContent = c < 0 ? "Match count unavailable" : `\u2717 Selection matches ${c} elements`;
|
|
1210
|
-
footer.style.color = "#fbbf24";
|
|
1667
|
+
let rel = file;
|
|
1668
|
+
try {
|
|
1669
|
+
rel = path.relative(process.cwd(), file);
|
|
1670
|
+
} catch {
|
|
1211
1671
|
}
|
|
1672
|
+
rel = rel.split(path.sep).join("/");
|
|
1673
|
+
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
1674
|
+
return `${rel}:${m[3]}:${m[4]}`;
|
|
1675
|
+
}
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/internal/capture/attachments.ts
|
|
1680
|
+
var ATTACHMENT_NAMES = {
|
|
1681
|
+
locators: "piwi-locators",
|
|
1682
|
+
ariaSnapshot: "piwi-aria-snapshot",
|
|
1683
|
+
console: "piwi-console",
|
|
1684
|
+
network: "piwi-network",
|
|
1685
|
+
webVitals: "piwi-web-vitals",
|
|
1686
|
+
locatorSuggestion: "piwi-locator-suggestion",
|
|
1687
|
+
pageState: "piwi-page-state",
|
|
1688
|
+
userPick: "piwi-user-pick"
|
|
1689
|
+
};
|
|
1690
|
+
var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
|
|
1691
|
+
var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
1692
|
+
var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
|
|
1693
|
+
|
|
1694
|
+
// src/internal/capture/inspect-on-failure.ts
|
|
1695
|
+
function isCi(ci) {
|
|
1696
|
+
return ci !== void 0 && ci !== "" && ci !== "false";
|
|
1697
|
+
}
|
|
1698
|
+
function shouldInspectOnFailure(gate) {
|
|
1699
|
+
if (gate.enabled !== "true") return false;
|
|
1700
|
+
if (isCi(gate.ci)) return false;
|
|
1701
|
+
if (gate.headless !== false) return false;
|
|
1702
|
+
if (gate.status !== "failed" && gate.status !== "timedOut") return false;
|
|
1703
|
+
if (gate.status === gate.expectedStatus) return false;
|
|
1704
|
+
return gate.retry >= gate.retries;
|
|
1705
|
+
}
|
|
1706
|
+
function environmentalSkipReason(gate) {
|
|
1707
|
+
if (gate.enabled !== "true") return null;
|
|
1708
|
+
if (gate.status !== "failed" && gate.status !== "timedOut") return null;
|
|
1709
|
+
if (gate.status === gate.expectedStatus) return null;
|
|
1710
|
+
if (isCi(gate.ci)) return "running under CI \u2014 this is a headed, local-only feature";
|
|
1711
|
+
if (gate.headless !== false) {
|
|
1712
|
+
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
1713
|
+
}
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1716
|
+
function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT_ON_FAIL) {
|
|
1717
|
+
const use = testInfo.project?.use ?? {};
|
|
1718
|
+
return {
|
|
1719
|
+
enabled,
|
|
1720
|
+
ci: process.env.CI,
|
|
1721
|
+
status: testInfo.status,
|
|
1722
|
+
expectedStatus: testInfo.expectedStatus,
|
|
1723
|
+
headless: use.headless,
|
|
1724
|
+
retry: testInfo.retry,
|
|
1725
|
+
retries: testInfo.project?.retries ?? 0
|
|
1212
1726
|
};
|
|
1213
|
-
rows.forEach((row, i) => {
|
|
1214
|
-
const line = doc.createElement("label");
|
|
1215
|
-
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"};`;
|
|
1216
|
-
const box = doc.createElement("input");
|
|
1217
|
-
box.type = "checkbox";
|
|
1218
|
-
box.disabled = !row.selectable;
|
|
1219
|
-
const text = doc.createElement("span");
|
|
1220
|
-
text.style.cssText = "flex:1;min-width:0;";
|
|
1221
|
-
const code = doc.createElement("code");
|
|
1222
|
-
code.style.cssText = "display:block;font:11px ui-monospace,monospace;color:#e5e7eb;word-break:break-all;";
|
|
1223
|
-
code.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
1224
|
-
const hint = doc.createElement("span");
|
|
1225
|
-
hint.style.cssText = "color:#9ca3af;";
|
|
1226
|
-
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";
|
|
1227
|
-
text.appendChild(code);
|
|
1228
|
-
text.appendChild(hint);
|
|
1229
|
-
line.appendChild(box);
|
|
1230
|
-
line.appendChild(text);
|
|
1231
|
-
line.addEventListener("mouseenter", () => {
|
|
1232
|
-
const r = row.node.getBoundingClientRect();
|
|
1233
|
-
outline.style.display = "block";
|
|
1234
|
-
outline.style.left = r.left + "px";
|
|
1235
|
-
outline.style.top = r.top + "px";
|
|
1236
|
-
outline.style.width = r.width + "px";
|
|
1237
|
-
outline.style.height = r.height + "px";
|
|
1238
|
-
});
|
|
1239
|
-
line.addEventListener("mouseleave", () => {
|
|
1240
|
-
outline.style.display = "none";
|
|
1241
|
-
});
|
|
1242
|
-
box.addEventListener("change", () => {
|
|
1243
|
-
if (box.checked) selected.add(i);
|
|
1244
|
-
else selected.delete(i);
|
|
1245
|
-
refreshFooter();
|
|
1246
|
-
});
|
|
1247
|
-
panel.appendChild(line);
|
|
1248
|
-
});
|
|
1249
|
-
panel.appendChild(footer);
|
|
1250
|
-
const cleanup = () => {
|
|
1251
|
-
doc.removeEventListener("keydown", onKey, true);
|
|
1252
|
-
panel.remove();
|
|
1253
|
-
outline.remove();
|
|
1254
|
-
pickedOutline.remove();
|
|
1255
|
-
};
|
|
1256
|
-
const done = (state) => {
|
|
1257
|
-
g.__piwiPickAnchors = state === "done" ? rows.filter((_, i) => selected.has(i)).map((r) => r.info) : [];
|
|
1258
|
-
g.__piwiAnchorState = state;
|
|
1259
|
-
cleanup();
|
|
1260
|
-
};
|
|
1261
|
-
const onKey = (e) => {
|
|
1262
|
-
if (e.key !== "Escape") return;
|
|
1263
|
-
e.preventDefault();
|
|
1264
|
-
e.stopImmediatePropagation();
|
|
1265
|
-
done("skipped");
|
|
1266
|
-
};
|
|
1267
|
-
const buttonRow = doc.createElement("div");
|
|
1268
|
-
buttonRow.style.cssText = "display:flex;gap:8px;margin-top:4px;";
|
|
1269
|
-
const useBtn = doc.createElement("button");
|
|
1270
|
-
useBtn.style.cssText = "flex:1;background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer;font:600 12px system-ui;";
|
|
1271
|
-
useBtn.textContent = "Use selected parents";
|
|
1272
|
-
useBtn.addEventListener("click", (e) => {
|
|
1273
|
-
e.preventDefault();
|
|
1274
|
-
e.stopImmediatePropagation();
|
|
1275
|
-
done(selected.size > 0 ? "done" : "skipped");
|
|
1276
|
-
});
|
|
1277
|
-
const skipBtn = doc.createElement("button");
|
|
1278
|
-
skipBtn.style.cssText = "background:none;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:8px 10px;cursor:pointer;font:12px system-ui;";
|
|
1279
|
-
skipBtn.textContent = "Skip (Esc)";
|
|
1280
|
-
skipBtn.addEventListener("click", (e) => {
|
|
1281
|
-
e.preventDefault();
|
|
1282
|
-
e.stopImmediatePropagation();
|
|
1283
|
-
done("skipped");
|
|
1284
|
-
});
|
|
1285
|
-
buttonRow.appendChild(useBtn);
|
|
1286
|
-
buttonRow.appendChild(skipBtn);
|
|
1287
|
-
panel.appendChild(buttonRow);
|
|
1288
|
-
refreshFooter();
|
|
1289
|
-
g.__piwiAnchorCleanup = cleanup;
|
|
1290
|
-
doc.addEventListener("keydown", onKey, true);
|
|
1291
|
-
doc.body.appendChild(pickedOutline);
|
|
1292
|
-
doc.body.appendChild(outline);
|
|
1293
|
-
doc.body.appendChild(panel);
|
|
1294
1727
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1728
|
+
|
|
1729
|
+
// src/internal/capture/pick-on-failure.ts
|
|
1730
|
+
var path2 = __toESM(require("path"));
|
|
1731
|
+
var ANSI_RE = /\[[0-9;]*m/g;
|
|
1732
|
+
function endOfString(s, start) {
|
|
1733
|
+
const q = s[start];
|
|
1734
|
+
for (let i = start + 1; i < s.length; i++) {
|
|
1735
|
+
if (s[i] === "\\") {
|
|
1736
|
+
i++;
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
if (s[i] === q) return i;
|
|
1740
|
+
}
|
|
1741
|
+
return s.length - 1;
|
|
1742
|
+
}
|
|
1743
|
+
function matchBrace(s, start) {
|
|
1744
|
+
let depth = 0;
|
|
1745
|
+
for (let i = start; i < s.length; i++) {
|
|
1746
|
+
if (s[i] === "{") depth++;
|
|
1747
|
+
else if (s[i] === "}" && --depth === 0) return i;
|
|
1748
|
+
}
|
|
1749
|
+
return s.length - 1;
|
|
1750
|
+
}
|
|
1751
|
+
function parseOptions(src) {
|
|
1752
|
+
const obj = {};
|
|
1753
|
+
const re = /(\w+)\s*:\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*"|true|false|-?\d+)/g;
|
|
1754
|
+
let m;
|
|
1755
|
+
while ((m = re.exec(src)) !== null) {
|
|
1756
|
+
const key = m[1];
|
|
1757
|
+
const raw = m[2];
|
|
1758
|
+
if (raw === "true") obj[key] = true;
|
|
1759
|
+
else if (raw === "false") obj[key] = false;
|
|
1760
|
+
else if (/^-?\d+$/.test(raw)) obj[key] = Number(raw);
|
|
1761
|
+
else obj[key] = raw.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
1762
|
+
}
|
|
1763
|
+
return obj;
|
|
1764
|
+
}
|
|
1765
|
+
function parseArgs(inner) {
|
|
1766
|
+
const args = [];
|
|
1767
|
+
let i = 0;
|
|
1768
|
+
while (i < inner.length) {
|
|
1769
|
+
const c = inner[i];
|
|
1770
|
+
if (c === " " || c === ",") {
|
|
1771
|
+
i++;
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
if (c === "'" || c === '"') {
|
|
1775
|
+
const end = endOfString(inner, i);
|
|
1776
|
+
args.push(inner.slice(i + 1, end).replace(/\\(.)/g, "$1"));
|
|
1777
|
+
i = end + 1;
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
if (c === "{") {
|
|
1781
|
+
const end = matchBrace(inner, i);
|
|
1782
|
+
args.push(parseOptions(inner.slice(i, end + 1)));
|
|
1783
|
+
i = end + 1;
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
i++;
|
|
1301
1787
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
last = re.lastIndex;
|
|
1788
|
+
return args;
|
|
1789
|
+
}
|
|
1790
|
+
function leafExpression(expr) {
|
|
1791
|
+
let depth = 0;
|
|
1792
|
+
let leafStart = 0;
|
|
1793
|
+
for (let i = 0; i < expr.length - 1; i++) {
|
|
1794
|
+
const c = expr[i];
|
|
1795
|
+
if (c === "'" || c === '"') {
|
|
1796
|
+
i = endOfString(expr, i);
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
if (c === "(") depth++;
|
|
1800
|
+
else if (c === ")") {
|
|
1801
|
+
depth--;
|
|
1802
|
+
if (depth === 0 && expr[i + 1] === ".") leafStart = i + 2;
|
|
1318
1803
|
}
|
|
1319
|
-
if (last < expr.length) html += escHtml(expr.slice(last));
|
|
1320
|
-
return html;
|
|
1321
|
-
};
|
|
1322
|
-
const title = doc.createElement("div");
|
|
1323
|
-
title.style.cssText = "font-weight:600;margin-bottom:4px;";
|
|
1324
|
-
title.textContent = arg.failing ? "Pick a replacement locator" : "Pick a locator";
|
|
1325
|
-
const sub = doc.createElement("div");
|
|
1326
|
-
sub.style.cssText = "color:#9ca3af;margin-bottom:12px;";
|
|
1327
|
-
if (arg.failing) {
|
|
1328
|
-
sub.innerHTML = `Replaces <code style="font-family:ui-monospace,Menlo,monospace">${hlLocator(arg.failing)}</code> \u2014 ranked by stability score.`;
|
|
1329
|
-
} else {
|
|
1330
|
-
sub.textContent = "For the element you picked \u2014 ranked by stability score.";
|
|
1331
1804
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
};
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
btn.appendChild(score);
|
|
1356
|
-
btn.addEventListener("click", (e) => {
|
|
1357
|
-
e.preventDefault();
|
|
1358
|
-
e.stopImmediatePropagation();
|
|
1359
|
-
done(i);
|
|
1360
|
-
});
|
|
1361
|
-
panel.appendChild(btn);
|
|
1362
|
-
});
|
|
1363
|
-
const skip = doc.createElement("button");
|
|
1364
|
-
skip.style.cssText = "background:none;border:none;color:#9ca3af;cursor:pointer;padding:6px 0 0;font:12px system-ui,sans-serif;";
|
|
1365
|
-
skip.textContent = "Skip \u2014 keep the failure as-is (Esc)";
|
|
1366
|
-
skip.addEventListener("click", (e) => {
|
|
1367
|
-
e.preventDefault();
|
|
1368
|
-
e.stopImmediatePropagation();
|
|
1369
|
-
done(-1);
|
|
1370
|
-
});
|
|
1371
|
-
panel.appendChild(skip);
|
|
1372
|
-
doc.addEventListener("keydown", onKey, true);
|
|
1373
|
-
wrap.appendChild(panel);
|
|
1374
|
-
doc.body.appendChild(wrap);
|
|
1805
|
+
return expr.slice(leafStart);
|
|
1806
|
+
}
|
|
1807
|
+
function parseLeafLocatorExpression(rawExpr) {
|
|
1808
|
+
const expr = leafExpression(rawExpr.trim());
|
|
1809
|
+
const m = /^([A-Za-z]+)\((.*)\)$/s.exec(expr);
|
|
1810
|
+
if (!m) return null;
|
|
1811
|
+
return { method: m[1], args: parseArgs(m[2].trim()) };
|
|
1812
|
+
}
|
|
1813
|
+
function deriveFailedLocator(testInfo) {
|
|
1814
|
+
const info = testInfo;
|
|
1815
|
+
const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
|
|
1816
|
+
for (const err of errors) {
|
|
1817
|
+
const text = `${err.message ?? ""}
|
|
1818
|
+
${err.stack ?? ""}`.replace(ANSI_RE, "");
|
|
1819
|
+
const line = /^\s*Locator:\s*(.+)$/m.exec(text);
|
|
1820
|
+
if (!line) continue;
|
|
1821
|
+
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
1822
|
+
if (!parsed) continue;
|
|
1823
|
+
const loc = err.location;
|
|
1824
|
+
const location = loc ? `${path2.relative(process.cwd(), loc.file).split(path2.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
1825
|
+
return { method: parsed.method, args: parsed.args, location };
|
|
1826
|
+
}
|
|
1827
|
+
return null;
|
|
1375
1828
|
}
|
|
1376
1829
|
async function cleanupPicker(page) {
|
|
1377
1830
|
try {
|
|
@@ -1399,7 +1852,8 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
|
1399
1852
|
`
|
|
1400
1853
|
[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).`
|
|
1401
1854
|
);
|
|
1402
|
-
|
|
1855
|
+
const overlayArg = { transport: "global", failing: rendered };
|
|
1856
|
+
await page.evaluate(installPickerOverlay, overlayArg);
|
|
1403
1857
|
await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
|
|
1404
1858
|
timeout: 0,
|
|
1405
1859
|
polling: 250
|
|
@@ -1420,9 +1874,9 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
|
1420
1874
|
if (role) {
|
|
1421
1875
|
const probeArg = probe.arg;
|
|
1422
1876
|
await page.evaluate(showAnchorPicker, {
|
|
1423
|
-
tagRoles: probeArg.tagRoles,
|
|
1424
|
-
inputRoles: probeArg.inputRoles,
|
|
1425
|
-
roleSources: probeArg.roleSources,
|
|
1877
|
+
tagRoles: probeArg.tagRoles ?? {},
|
|
1878
|
+
inputRoles: probeArg.inputRoles ?? {},
|
|
1879
|
+
roleSources: probeArg.roleSources ?? "",
|
|
1426
1880
|
leafRole: role,
|
|
1427
1881
|
leafLevel: level,
|
|
1428
1882
|
leafTestId: attrs.attributes["data-testid"] ?? null
|
|
@@ -1787,7 +2241,12 @@ var CAPTURED_ATTRS_ARG = {
|
|
|
1787
2241
|
inputRoles: INPUT_TYPE_TO_ROLE,
|
|
1788
2242
|
// '[role]' plus every tag the maps can resolve (input/select are handled by
|
|
1789
2243
|
// special-cased logic in the probe, so add them explicitly).
|
|
1790
|
-
roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(",")
|
|
2244
|
+
roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
|
|
2245
|
+
// The reporter always wants ancestor-anchored alternatives (the picker's
|
|
2246
|
+
// anchors step and generateAnchoredAlternatives both need them); it derives
|
|
2247
|
+
// the accessible name itself, so the probe's own labelText is unneeded.
|
|
2248
|
+
includeStructural: true,
|
|
2249
|
+
includeLabelText: false
|
|
1791
2250
|
};
|
|
1792
2251
|
async function ariaSnapshotBestEffort(target, timeout) {
|
|
1793
2252
|
if (typeof target.ariaSnapshot !== "function") return null;
|
|
@@ -1807,156 +2266,6 @@ async function ariaSnapshotBestEffort(target, timeout) {
|
|
|
1807
2266
|
}
|
|
1808
2267
|
}
|
|
1809
2268
|
}
|
|
1810
|
-
function probeElementAttrs(el, arg) {
|
|
1811
|
-
const { keep, tagRoles, inputRoles, roleSources } = arg;
|
|
1812
|
-
const attrMap = {};
|
|
1813
|
-
for (const key of keep) {
|
|
1814
|
-
const v = el.getAttribute(key) ?? el[key];
|
|
1815
|
-
attrMap[key] = typeof v === "string" ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
|
|
1816
|
-
}
|
|
1817
|
-
const r = el.getBoundingClientRect();
|
|
1818
|
-
const selectorCounts = {};
|
|
1819
|
-
try {
|
|
1820
|
-
const doc = el.ownerDocument;
|
|
1821
|
-
const cssEsc = (s) => doc.defaultView.CSS.escape(s);
|
|
1822
|
-
const count = (sel) => {
|
|
1823
|
-
try {
|
|
1824
|
-
return doc.querySelectorAll(sel).length;
|
|
1825
|
-
} catch {
|
|
1826
|
-
return void 0;
|
|
1827
|
-
}
|
|
1828
|
-
};
|
|
1829
|
-
if (attrMap["data-testid"]) {
|
|
1830
|
-
selectorCounts.testId = count(`[data-testid=${JSON.stringify(attrMap["data-testid"])}]`);
|
|
1831
|
-
}
|
|
1832
|
-
if (attrMap["id"]) selectorCounts.id = count(`#${cssEsc(attrMap["id"])}`);
|
|
1833
|
-
if (attrMap["name"]) selectorCounts.name = count(`[name=${JSON.stringify(attrMap["name"])}]`);
|
|
1834
|
-
const classList = (attrMap["class"] || "").split(/\s+/).filter((c) => c.length > 1).slice(0, 10);
|
|
1835
|
-
if (classList.length > 0) {
|
|
1836
|
-
const classCounts = {};
|
|
1837
|
-
for (const cls of classList) {
|
|
1838
|
-
const n = count(`.${cssEsc(cls)}`);
|
|
1839
|
-
if (n !== void 0) classCounts[cls] = n;
|
|
1840
|
-
}
|
|
1841
|
-
selectorCounts.classes = classCounts;
|
|
1842
|
-
}
|
|
1843
|
-
} catch {
|
|
1844
|
-
}
|
|
1845
|
-
let rolePosition = null;
|
|
1846
|
-
const ancestors = [];
|
|
1847
|
-
try {
|
|
1848
|
-
const doc = el.ownerDocument;
|
|
1849
|
-
const cssEsc = (s) => doc.defaultView.CSS.escape(s);
|
|
1850
|
-
const count = (sel) => {
|
|
1851
|
-
try {
|
|
1852
|
-
return doc.querySelectorAll(sel).length;
|
|
1853
|
-
} catch {
|
|
1854
|
-
return void 0;
|
|
1855
|
-
}
|
|
1856
|
-
};
|
|
1857
|
-
const roleOf = (n) => {
|
|
1858
|
-
const explicit = n.getAttribute("role");
|
|
1859
|
-
if (explicit) return explicit;
|
|
1860
|
-
const tag = (n.tagName || "").toLowerCase();
|
|
1861
|
-
if (tag === "input") return inputRoles[(n.getAttribute("type") || "text").toLowerCase()] ?? "textbox";
|
|
1862
|
-
if (tag === "select") return n.getAttribute("multiple") != null ? "listbox" : "combobox";
|
|
1863
|
-
if (tag === "a") return n.getAttribute("href") != null ? "link" : null;
|
|
1864
|
-
return tagRoles[tag] ?? null;
|
|
1865
|
-
};
|
|
1866
|
-
const levelOf = (n) => {
|
|
1867
|
-
const m = /^h([1-6])$/.exec((n.tagName || "").toLowerCase());
|
|
1868
|
-
if (m) return Number(m[1]);
|
|
1869
|
-
const al = n.getAttribute("aria-level");
|
|
1870
|
-
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
1871
|
-
};
|
|
1872
|
-
const targetRole = roleOf(el);
|
|
1873
|
-
const targetLevel = targetRole === "heading" ? levelOf(el) : null;
|
|
1874
|
-
if (targetRole) {
|
|
1875
|
-
const nodes = doc.querySelectorAll(roleSources);
|
|
1876
|
-
if (nodes.length <= 4e3) {
|
|
1877
|
-
let roleCountAll = 0;
|
|
1878
|
-
let index = -1;
|
|
1879
|
-
let levelCount = 0;
|
|
1880
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1881
|
-
const n = nodes[i];
|
|
1882
|
-
if (roleOf(n) !== targetRole) continue;
|
|
1883
|
-
if (n === el) index = roleCountAll;
|
|
1884
|
-
roleCountAll++;
|
|
1885
|
-
if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
|
|
1886
|
-
}
|
|
1887
|
-
if (index !== -1) {
|
|
1888
|
-
rolePosition = {
|
|
1889
|
-
role: targetRole,
|
|
1890
|
-
count: roleCountAll,
|
|
1891
|
-
index,
|
|
1892
|
-
...targetLevel != null ? { levelCount } : {}
|
|
1893
|
-
};
|
|
1894
|
-
}
|
|
1895
|
-
const CONTAINER_TAGS = ["form", "nav", "main", "article", "section", "dialog", "table"];
|
|
1896
|
-
const docRoleCount = (role) => {
|
|
1897
|
-
let c = 0;
|
|
1898
|
-
for (let i = 0; i < nodes.length; i++) if (roleOf(nodes[i]) === role) c++;
|
|
1899
|
-
return c;
|
|
1900
|
-
};
|
|
1901
|
-
let node = el.parentElement;
|
|
1902
|
-
let depth = 0;
|
|
1903
|
-
while (node && depth < 12 && ancestors.length < 4) {
|
|
1904
|
-
depth++;
|
|
1905
|
-
const tag = (node.tagName || "").toLowerCase();
|
|
1906
|
-
if (tag === "body" || tag === "html") break;
|
|
1907
|
-
const testId = node.getAttribute("data-testid");
|
|
1908
|
-
const id = node.getAttribute("id");
|
|
1909
|
-
const explicitRole = node.getAttribute("role");
|
|
1910
|
-
const ariaLabel = node.getAttribute("aria-label");
|
|
1911
|
-
const anchorRole = explicitRole || (CONTAINER_TAGS.includes(tag) ? tagRoles[tag] : null) || null;
|
|
1912
|
-
if (testId || id || anchorRole || ariaLabel) {
|
|
1913
|
-
const scoped = node.querySelectorAll(roleSources);
|
|
1914
|
-
let scopedRoleCount = 0;
|
|
1915
|
-
if (scoped.length <= 2e3) {
|
|
1916
|
-
for (let i = 0; i < scoped.length; i++) {
|
|
1917
|
-
const n = scoped[i];
|
|
1918
|
-
if (roleOf(n) !== targetRole) continue;
|
|
1919
|
-
if (targetLevel != null && levelOf(n) !== targetLevel) continue;
|
|
1920
|
-
scopedRoleCount++;
|
|
1921
|
-
}
|
|
1922
|
-
} else {
|
|
1923
|
-
scopedRoleCount = -1;
|
|
1924
|
-
}
|
|
1925
|
-
ancestors.push({
|
|
1926
|
-
tag,
|
|
1927
|
-
depth,
|
|
1928
|
-
testId: testId || null,
|
|
1929
|
-
id: id || null,
|
|
1930
|
-
role: explicitRole || null,
|
|
1931
|
-
ariaLabel: ariaLabel || null,
|
|
1932
|
-
...scopedRoleCount >= 0 ? { scopedRoleCount } : {},
|
|
1933
|
-
...testId ? { testIdCount: count(`[data-testid=${JSON.stringify(testId)}]`) } : {},
|
|
1934
|
-
...id ? { idCount: count(`#${cssEsc(id)}`) } : {},
|
|
1935
|
-
...anchorRole ? { roleCount: docRoleCount(anchorRole) } : {}
|
|
1936
|
-
});
|
|
1937
|
-
}
|
|
1938
|
-
node = node.parentElement;
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
} catch {
|
|
1943
|
-
}
|
|
1944
|
-
return {
|
|
1945
|
-
tagName: el.tagName?.toLowerCase?.() ?? "unknown",
|
|
1946
|
-
attributes: attrMap,
|
|
1947
|
-
// Collapse whitespace so multi-line text can't produce a getByText
|
|
1948
|
-
// suggestion with literal newlines in it.
|
|
1949
|
-
textContent: (el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80),
|
|
1950
|
-
center: {
|
|
1951
|
-
x: Math.round(r.x + r.width / 2),
|
|
1952
|
-
y: Math.round(r.y + r.height / 2)
|
|
1953
|
-
},
|
|
1954
|
-
hasLabel: !!(el.labels && el.labels.length > 0),
|
|
1955
|
-
selectorCounts,
|
|
1956
|
-
rolePosition,
|
|
1957
|
-
ancestors
|
|
1958
|
-
};
|
|
1959
|
-
}
|
|
1960
2269
|
function startElementCapture(sink, target, seq, callerLocation, used) {
|
|
1961
2270
|
const probe = target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
|
|
1962
2271
|
const settledProbe = probe.then(
|