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