@unotest/web 0.5.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/.claude/skills/write-e2e-test.md +551 -0
- package/CHANGELOG.md +158 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/bin/unotest-web.js +197 -0
- package/dist/config/schema.d.ts +356 -0
- package/dist/config/schema.js +1 -0
- package/dist/driver/index.d.ts +74 -0
- package/dist/driver/index.js +1 -0
- package/dist/dsl/index.d.ts +338 -0
- package/dist/dsl/index.js +1 -0
- package/dist/inspection/page-inject.d.ts +395 -0
- package/dist/inspection/page-inject.js +3533 -0
- package/dist/interfaces-iTfjd1zT.d.ts +407 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +1 -0
- package/dist/runner/cli.d.ts +22 -0
- package/dist/runner/cli.js +1 -0
- package/dist/runner/init.d.ts +6 -0
- package/dist/runner/init.js +1 -0
- package/dist/runner/install-chromium.d.ts +9 -0
- package/dist/runner/install-chromium.js +1 -0
- package/dist/runner/prepare-fix.d.ts +8 -0
- package/dist/runner/prepare-fix.js +1 -0
- package/dist/runner/serve-fixture.d.ts +2 -0
- package/dist/runner/serve-fixture.js +1 -0
- package/dist/runner/web-runner-adapter.d.ts +8 -0
- package/dist/runner/web-runner-adapter.js +1 -0
- package/examples/fixtures/break-fix-app/README.md +45 -0
- package/examples/fixtures/break-fix-app/index.html +101 -0
- package/examples/fixtures/break-fix-app/serve.mjs +95 -0
- package/package.json +115 -0
- package/src/mcp/prompts/agent-test-author.md +208 -0
|
@@ -0,0 +1,3533 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/semantic-dom/algorithm.ts
|
|
5
|
+
function semanticDomAlgorithm(options) {
|
|
6
|
+
const disambiguateEnabled = options?.disambiguate !== false;
|
|
7
|
+
const INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
|
|
8
|
+
"a",
|
|
9
|
+
"button",
|
|
10
|
+
"input",
|
|
11
|
+
"select",
|
|
12
|
+
"textarea",
|
|
13
|
+
"details",
|
|
14
|
+
"summary"
|
|
15
|
+
]);
|
|
16
|
+
const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
17
|
+
"button",
|
|
18
|
+
"link",
|
|
19
|
+
"menuitem",
|
|
20
|
+
"tab",
|
|
21
|
+
"checkbox",
|
|
22
|
+
"radio",
|
|
23
|
+
"combobox",
|
|
24
|
+
"searchbox",
|
|
25
|
+
"textbox",
|
|
26
|
+
"switch",
|
|
27
|
+
"treeitem",
|
|
28
|
+
"menuitemcheckbox",
|
|
29
|
+
"menuitemradio",
|
|
30
|
+
"option"
|
|
31
|
+
]);
|
|
32
|
+
const LANDMARK_TAGS = /* @__PURE__ */ new Set([
|
|
33
|
+
"header",
|
|
34
|
+
"nav",
|
|
35
|
+
"main",
|
|
36
|
+
"aside",
|
|
37
|
+
"footer",
|
|
38
|
+
"form",
|
|
39
|
+
"section",
|
|
40
|
+
"article",
|
|
41
|
+
"dialog"
|
|
42
|
+
]);
|
|
43
|
+
const LANDMARK_ROLES = /* @__PURE__ */ new Set([
|
|
44
|
+
"banner",
|
|
45
|
+
"navigation",
|
|
46
|
+
"main",
|
|
47
|
+
"contentinfo",
|
|
48
|
+
"search",
|
|
49
|
+
"region",
|
|
50
|
+
"complementary",
|
|
51
|
+
"dialog",
|
|
52
|
+
"alertdialog"
|
|
53
|
+
]);
|
|
54
|
+
const CMP_SELECTORS = [
|
|
55
|
+
"#onetrust-banner-sdk",
|
|
56
|
+
"#onetrust-consent-sdk",
|
|
57
|
+
"#CybotCookiebotDialog",
|
|
58
|
+
"#didomi-notice",
|
|
59
|
+
"#didomi-host",
|
|
60
|
+
"#usercentrics-root",
|
|
61
|
+
'[id^="usercentrics"]',
|
|
62
|
+
"#truste-consent-track",
|
|
63
|
+
"#truste-consent-content",
|
|
64
|
+
".qc-cmp2-container",
|
|
65
|
+
"#cmpbox",
|
|
66
|
+
'[data-testid="cookie-banner"]',
|
|
67
|
+
'[data-testid="consent-banner"]'
|
|
68
|
+
];
|
|
69
|
+
const nodes = [];
|
|
70
|
+
const elementToIndex = /* @__PURE__ */ new Map();
|
|
71
|
+
let shadowHostCount = 0;
|
|
72
|
+
let shadowElementCount = 0;
|
|
73
|
+
let visibleCount = 0;
|
|
74
|
+
let interestingCount = 0;
|
|
75
|
+
let customElementCount = 0;
|
|
76
|
+
let walkVisits = 0;
|
|
77
|
+
function isDynamicId(id) {
|
|
78
|
+
if (/\d{4,}/.test(id)) return true;
|
|
79
|
+
if (id.includes(":")) return true;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
__name(isDynamicId, "isDynamicId");
|
|
83
|
+
function stripRandomTail(s) {
|
|
84
|
+
if (!s) return s || "";
|
|
85
|
+
let out = s;
|
|
86
|
+
out = out.replace(/([-_])([A-Za-z0-9]{8,})(?=["\]\s]|$)/g, (_m, sep, tail) => {
|
|
87
|
+
if (/^[a-z]+$/.test(tail) || /^[A-Z]+$/.test(tail)) return `${sep}${tail}`;
|
|
88
|
+
const transitions = (tail.match(/[a-z][A-Z]|[A-Z][a-z]/g) || []).length;
|
|
89
|
+
if (transitions <= 2 && !/\d/.test(tail)) return `${sep}${tail}`;
|
|
90
|
+
return `${sep}*`;
|
|
91
|
+
});
|
|
92
|
+
out = out.replace(/\/\d{5,}(?:[_-]\d+)*(?=\/|"|\]|$)/g, "/*");
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
__name(stripRandomTail, "stripRandomTail");
|
|
96
|
+
function isNoiseSample(sample, tag) {
|
|
97
|
+
if (!sample) return true;
|
|
98
|
+
const s = sample.trim();
|
|
99
|
+
if (!s) return true;
|
|
100
|
+
if (s === tag) return true;
|
|
101
|
+
if (s.toLowerCase() === tag.toLowerCase()) return true;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
__name(isNoiseSample, "isNoiseSample");
|
|
105
|
+
function dedupStrings(arr) {
|
|
106
|
+
const seen = /* @__PURE__ */ new Set();
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const s of arr) {
|
|
109
|
+
if (s && !seen.has(s)) {
|
|
110
|
+
seen.add(s);
|
|
111
|
+
out.push(s);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
__name(dedupStrings, "dedupStrings");
|
|
117
|
+
function pickPatternSamples(nodes2, count) {
|
|
118
|
+
const cap = count >= 20 ? 6 : count >= 5 ? 4 : count >= 3 ? 3 : 2;
|
|
119
|
+
const raw = nodes2.map((n) => (n.text || "").slice(0, 50) || n.tag);
|
|
120
|
+
const unique = dedupStrings(raw);
|
|
121
|
+
if (unique.length <= cap) return unique;
|
|
122
|
+
const indexed = unique.map((s, i) => ({ s, i }));
|
|
123
|
+
indexed.sort((a, b) => a.s.length - b.s.length || a.i - b.i);
|
|
124
|
+
return indexed.slice(0, cap).map((x) => x.s);
|
|
125
|
+
}
|
|
126
|
+
__name(pickPatternSamples, "pickPatternSamples");
|
|
127
|
+
let cursorCheckBudget = 2e3;
|
|
128
|
+
const CURSOR_CHECK_TAGS = /* @__PURE__ */ new Set(["span", "div", "li"]);
|
|
129
|
+
function isCursorPointerInteractive(el) {
|
|
130
|
+
if (cursorCheckBudget <= 0) return false;
|
|
131
|
+
const tag = el.tagName.toLowerCase();
|
|
132
|
+
if (!CURSOR_CHECK_TAGS.has(tag)) return false;
|
|
133
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
134
|
+
const childTag = el.children[i].tagName.toLowerCase();
|
|
135
|
+
if (INTERACTIVE_TAGS.has(childTag)) return false;
|
|
136
|
+
}
|
|
137
|
+
let ancestor = el.parentElement;
|
|
138
|
+
while (ancestor) {
|
|
139
|
+
const aTag = ancestor.tagName.toLowerCase();
|
|
140
|
+
if (INTERACTIVE_TAGS.has(aTag)) return false;
|
|
141
|
+
if (LANDMARK_TAGS.has(aTag) || aTag === "body" || aTag === "ul" || aTag === "ol") break;
|
|
142
|
+
ancestor = ancestor.parentElement;
|
|
143
|
+
}
|
|
144
|
+
cursorCheckBudget--;
|
|
145
|
+
const cursor = window.getComputedStyle(el).cursor;
|
|
146
|
+
if (cursor !== "pointer") return false;
|
|
147
|
+
ancestor = el.parentElement;
|
|
148
|
+
while (ancestor) {
|
|
149
|
+
const aTag = ancestor.tagName.toLowerCase();
|
|
150
|
+
if (!CURSOR_CHECK_TAGS.has(aTag)) break;
|
|
151
|
+
cursorCheckBudget--;
|
|
152
|
+
if (cursorCheckBudget <= 0) break;
|
|
153
|
+
if (window.getComputedStyle(ancestor).cursor === "pointer") return false;
|
|
154
|
+
ancestor = ancestor.parentElement;
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
__name(isCursorPointerInteractive, "isCursorPointerInteractive");
|
|
159
|
+
function isInteractive(el) {
|
|
160
|
+
const tag = el.tagName.toLowerCase();
|
|
161
|
+
if (INTERACTIVE_TAGS.has(tag)) return true;
|
|
162
|
+
const role = el.getAttribute("role");
|
|
163
|
+
if (role && INTERACTIVE_ROLES.has(role)) return true;
|
|
164
|
+
if (el.hasAttribute("aria-expanded")) return true;
|
|
165
|
+
if (el.hasAttribute("aria-haspopup") && el.getAttribute("aria-haspopup") !== "false") return true;
|
|
166
|
+
if (isCursorPointerInteractive(el)) return true;
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
__name(isInteractive, "isInteractive");
|
|
170
|
+
function isActionableHost(el) {
|
|
171
|
+
const tag = el.tagName.toLowerCase();
|
|
172
|
+
if (!tag.includes("-")) return false;
|
|
173
|
+
if (el.getAttribute("data-qa")) return true;
|
|
174
|
+
if (el.getAttribute("data-testid")) return true;
|
|
175
|
+
if (el.hasAttribute("tabindex") && el.getAttribute("tabindex") !== "-1") return true;
|
|
176
|
+
if (el.getAttribute("aria-label")) return true;
|
|
177
|
+
if (el.hasAttribute("onclick")) return true;
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
__name(isActionableHost, "isActionableHost");
|
|
181
|
+
function isInteresting(el) {
|
|
182
|
+
if (isInteractive(el)) return true;
|
|
183
|
+
const tag = el.tagName.toLowerCase();
|
|
184
|
+
if (LANDMARK_TAGS.has(tag)) return true;
|
|
185
|
+
const role = el.getAttribute("role");
|
|
186
|
+
if (role && LANDMARK_ROLES.has(role)) return true;
|
|
187
|
+
if (tag.includes("-")) return true;
|
|
188
|
+
if (el.children.length >= 3) {
|
|
189
|
+
let interactiveDescCount = 0;
|
|
190
|
+
for (let i = 0; i < el.children.length && interactiveDescCount < 2; i++) {
|
|
191
|
+
if (hasInteractiveDescendant(el.children[i])) {
|
|
192
|
+
interactiveDescCount++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (interactiveDescCount >= 2) return true;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
__name(isInteresting, "isInteresting");
|
|
200
|
+
function hasInteractiveDescendant(el) {
|
|
201
|
+
if (isInteractive(el)) return true;
|
|
202
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
203
|
+
if (hasInteractiveDescendant(el.children[i])) return true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
__name(hasInteractiveDescendant, "hasInteractiveDescendant");
|
|
208
|
+
function isVisible(el) {
|
|
209
|
+
const rect = el.getBoundingClientRect();
|
|
210
|
+
if (rect.height > 0 && rect.width > 0) return true;
|
|
211
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
212
|
+
const c = el.children[i].getBoundingClientRect();
|
|
213
|
+
if (c.width > 0 && c.height > 0) return true;
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
__name(isVisible, "isVisible");
|
|
218
|
+
function buildSelectorWithStability(el, shadowHostSelector) {
|
|
219
|
+
const tag = el.tagName.toLowerCase();
|
|
220
|
+
let sel;
|
|
221
|
+
let stability = "low";
|
|
222
|
+
const id = el.id;
|
|
223
|
+
if (id && !isDynamicId(id)) {
|
|
224
|
+
sel = `#${id}`;
|
|
225
|
+
stability = "high";
|
|
226
|
+
} else {
|
|
227
|
+
const qa = el.getAttribute("data-qa");
|
|
228
|
+
if (qa) {
|
|
229
|
+
sel = `[data-qa="${qa}"]`;
|
|
230
|
+
stability = "high";
|
|
231
|
+
} else {
|
|
232
|
+
const testId = el.getAttribute("data-testid");
|
|
233
|
+
if (testId) {
|
|
234
|
+
sel = `[data-testid="${testId}"]`;
|
|
235
|
+
stability = "high";
|
|
236
|
+
} else {
|
|
237
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
238
|
+
if (ariaLabel) {
|
|
239
|
+
sel = `${tag}[aria-label="${ariaLabel.slice(0, 60)}"]`;
|
|
240
|
+
stability = "medium";
|
|
241
|
+
} else {
|
|
242
|
+
const name = el.getAttribute("name");
|
|
243
|
+
if (name) {
|
|
244
|
+
sel = `${tag}[name="${name}"]`;
|
|
245
|
+
stability = "medium";
|
|
246
|
+
} else {
|
|
247
|
+
const placeholder = el.placeholder;
|
|
248
|
+
if (placeholder) {
|
|
249
|
+
sel = `${tag}[placeholder="${placeholder.slice(0, 40)}"]`;
|
|
250
|
+
stability = "medium";
|
|
251
|
+
} else {
|
|
252
|
+
const type = el.type;
|
|
253
|
+
if (tag === "input" && type) {
|
|
254
|
+
sel = `input[type="${type}"]`;
|
|
255
|
+
stability = "medium";
|
|
256
|
+
} else {
|
|
257
|
+
const rawHref = tag === "a" ? el.getAttribute("href") : null;
|
|
258
|
+
const hrefStart = rawHref == null ? -1 : rawHref.search(/[?#]/);
|
|
259
|
+
const cleanHref = rawHref == null ? "" : hrefStart >= 0 ? rawHref.slice(0, hrefStart) : rawHref;
|
|
260
|
+
const hrefUsable = rawHref && rawHref !== "#" && rawHref !== "/" && cleanHref !== "" && cleanHref !== "/";
|
|
261
|
+
if (hrefUsable) {
|
|
262
|
+
const stripped = hrefStart >= 0;
|
|
263
|
+
if (cleanHref.length <= 120) {
|
|
264
|
+
sel = stripped ? `a[href^="${cleanHref}"]` : `a[href="${cleanHref}"]`;
|
|
265
|
+
stability = "high";
|
|
266
|
+
} else {
|
|
267
|
+
sel = `a[href^="${cleanHref.slice(0, 120)}"]`;
|
|
268
|
+
stability = "medium";
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
sel = textLocatorFallback(el, tag);
|
|
272
|
+
stability = "low";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (shadowHostSelector) {
|
|
282
|
+
return { selector: `${shadowHostSelector} >> ${sel}`, stability };
|
|
283
|
+
}
|
|
284
|
+
return { selector: sel, stability };
|
|
285
|
+
}
|
|
286
|
+
__name(buildSelectorWithStability, "buildSelectorWithStability");
|
|
287
|
+
function shortTextChunk(rawText2) {
|
|
288
|
+
let normalized = rawText2.replace(/\s+/g, " ").trim();
|
|
289
|
+
if (!normalized) return "";
|
|
290
|
+
if (normalized.length >= 6) {
|
|
291
|
+
const half = Math.floor(normalized.length / 2);
|
|
292
|
+
if (normalized.slice(0, half) === normalized.slice(half)) {
|
|
293
|
+
normalized = normalized.slice(0, half);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (normalized.length <= 30) return normalized;
|
|
297
|
+
const leadingWordy = normalized.match(/^[\p{L}\p{N} &'.()\-/]+/u);
|
|
298
|
+
const wordsPart = leadingWordy ? leadingWordy[0].trim() : "";
|
|
299
|
+
if (!wordsPart) return normalized.slice(0, 15);
|
|
300
|
+
const words = wordsPart.split(" ").filter(Boolean);
|
|
301
|
+
if (words.length === 0) return "";
|
|
302
|
+
let chunk = words[0];
|
|
303
|
+
for (let i = 1; i < words.length && chunk.length < 10; i++) {
|
|
304
|
+
chunk = `${chunk} ${words[i]}`;
|
|
305
|
+
}
|
|
306
|
+
return chunk.slice(0, 25);
|
|
307
|
+
}
|
|
308
|
+
__name(shortTextChunk, "shortTextChunk");
|
|
309
|
+
function textLocatorFallback(el, tag) {
|
|
310
|
+
const rawText2 = el.textContent || "";
|
|
311
|
+
const chunk = shortTextChunk(rawText2);
|
|
312
|
+
if (chunk) {
|
|
313
|
+
const escaped = chunk.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
314
|
+
return `${tag}:has-text("${escaped}")`;
|
|
315
|
+
}
|
|
316
|
+
return tag;
|
|
317
|
+
}
|
|
318
|
+
__name(textLocatorFallback, "textLocatorFallback");
|
|
319
|
+
function buildSelector(el, shadowHostSelector) {
|
|
320
|
+
return buildSelectorWithStability(el, shadowHostSelector).selector;
|
|
321
|
+
}
|
|
322
|
+
__name(buildSelector, "buildSelector");
|
|
323
|
+
function buildLightNode(el, parentIdx, shadowHostSelector) {
|
|
324
|
+
const tag = el.tagName.toLowerCase();
|
|
325
|
+
const interactive = isInteractive(el);
|
|
326
|
+
let text2 = el.textContent?.trim().slice(0, 80) || "";
|
|
327
|
+
if (text2.length >= 6) {
|
|
328
|
+
const half = Math.floor(text2.length / 2);
|
|
329
|
+
if (text2.slice(0, half) === text2.slice(half)) text2 = text2.slice(0, half);
|
|
330
|
+
}
|
|
331
|
+
let interactiveChildCount = 0;
|
|
332
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
333
|
+
if (isInteractive(el.children[i])) interactiveChildCount++;
|
|
334
|
+
}
|
|
335
|
+
const childTags = [];
|
|
336
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
337
|
+
childTags.push(el.children[i].tagName.toLowerCase());
|
|
338
|
+
}
|
|
339
|
+
const node = {
|
|
340
|
+
tag,
|
|
341
|
+
interactive,
|
|
342
|
+
exportAsActionableHost: isActionableHost(el),
|
|
343
|
+
childSignature: childTags.join(","),
|
|
344
|
+
childCount: el.children.length,
|
|
345
|
+
interactiveChildCount,
|
|
346
|
+
element: el,
|
|
347
|
+
parentIndex: parentIdx,
|
|
348
|
+
children: []
|
|
349
|
+
};
|
|
350
|
+
if (el.id && !isDynamicId(el.id)) node.id = el.id;
|
|
351
|
+
const qa = el.getAttribute("data-qa");
|
|
352
|
+
if (qa) node.qa = qa;
|
|
353
|
+
const testId = el.getAttribute("data-testid");
|
|
354
|
+
if (testId) node.testId = testId;
|
|
355
|
+
const role = el.getAttribute("role");
|
|
356
|
+
if (role) node.role = role;
|
|
357
|
+
if (text2) node.text = text2;
|
|
358
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
359
|
+
if (ariaLabel) node.ariaLabel = ariaLabel.slice(0, 80);
|
|
360
|
+
if (tag === "a") {
|
|
361
|
+
try {
|
|
362
|
+
const href = el.href;
|
|
363
|
+
if (href) node.href = new URL(href).pathname.slice(0, 80);
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (tag === "input") {
|
|
368
|
+
node.type = el.type || "text";
|
|
369
|
+
}
|
|
370
|
+
if (shadowHostSelector) {
|
|
371
|
+
node.shadowHost = shadowHostSelector;
|
|
372
|
+
shadowElementCount++;
|
|
373
|
+
}
|
|
374
|
+
const nodeIndex = nodes.length;
|
|
375
|
+
nodes.push(node);
|
|
376
|
+
elementToIndex.set(el, nodeIndex);
|
|
377
|
+
if (parentIdx >= 0 && nodes[parentIdx]) {
|
|
378
|
+
nodes[parentIdx].children.push(nodeIndex);
|
|
379
|
+
}
|
|
380
|
+
return nodeIndex;
|
|
381
|
+
}
|
|
382
|
+
__name(buildLightNode, "buildLightNode");
|
|
383
|
+
function walkDOM(root, parentIdx, shadowHostSelector) {
|
|
384
|
+
const children = root.children;
|
|
385
|
+
for (let i = 0; i < children.length; i++) {
|
|
386
|
+
const el = children[i];
|
|
387
|
+
if (!isVisible(el)) continue;
|
|
388
|
+
walkVisits++;
|
|
389
|
+
visibleCount++;
|
|
390
|
+
const tag = el.tagName.toLowerCase();
|
|
391
|
+
if (tag.includes("-")) customElementCount++;
|
|
392
|
+
const interesting = isInteresting(el);
|
|
393
|
+
let currentIdx = parentIdx;
|
|
394
|
+
if (interesting) {
|
|
395
|
+
interestingCount++;
|
|
396
|
+
currentIdx = buildLightNode(el, parentIdx, shadowHostSelector);
|
|
397
|
+
}
|
|
398
|
+
const sr = el.shadowRoot;
|
|
399
|
+
if (sr) {
|
|
400
|
+
shadowHostCount++;
|
|
401
|
+
const hostSel = buildSelector(el, shadowHostSelector);
|
|
402
|
+
walkDOM(sr, currentIdx, hostSel);
|
|
403
|
+
}
|
|
404
|
+
walkDOM(el, currentIdx, shadowHostSelector);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
__name(walkDOM, "walkDOM");
|
|
408
|
+
const walkRoot = document.body || document.documentElement;
|
|
409
|
+
if (walkRoot) walkDOM(walkRoot, -1);
|
|
410
|
+
function collectShadowExportable(nodeIdx, out) {
|
|
411
|
+
const n = nodes[nodeIdx];
|
|
412
|
+
if (n.tag.includes("-")) return;
|
|
413
|
+
const isShadowDesc = n.shadowHost !== void 0;
|
|
414
|
+
if (isShadowDesc && !n.suppressedByHost && (n.interactive || n.exportAsActionableHost)) {
|
|
415
|
+
out.push(nodeIdx);
|
|
416
|
+
}
|
|
417
|
+
for (const ci of n.children) collectShadowExportable(ci, out);
|
|
418
|
+
}
|
|
419
|
+
__name(collectShadowExportable, "collectShadowExportable");
|
|
420
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
421
|
+
const host = nodes[i];
|
|
422
|
+
if (!host.tag.includes("-")) continue;
|
|
423
|
+
if (host.interactive) continue;
|
|
424
|
+
const hostOwnShadowSel = buildSelector(host.element, host.shadowHost);
|
|
425
|
+
const shadowLeaves = [];
|
|
426
|
+
for (const ci of host.children) {
|
|
427
|
+
if (nodes[ci].shadowHost === hostOwnShadowSel) {
|
|
428
|
+
collectShadowExportable(ci, shadowLeaves);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (shadowLeaves.length === 1) {
|
|
432
|
+
if (!host.exportAsActionableHost) host.exportAsActionableHost = true;
|
|
433
|
+
nodes[shadowLeaves[0]].suppressedByHost = true;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const totalInteractive = nodes.filter(
|
|
437
|
+
(n) => !n.suppressedByHost && (n.interactive || n.exportAsActionableHost)
|
|
438
|
+
).length;
|
|
439
|
+
let actionableHostCount = 0;
|
|
440
|
+
let suppressedByHostCount = 0;
|
|
441
|
+
for (const n of nodes) {
|
|
442
|
+
if (n.exportAsActionableHost) actionableHostCount++;
|
|
443
|
+
if (n.suppressedByHost) suppressedByHostCount++;
|
|
444
|
+
}
|
|
445
|
+
function computeDepth2Signature(nodeIdx) {
|
|
446
|
+
const node = nodes[nodeIdx];
|
|
447
|
+
if (node.children.length === 0) {
|
|
448
|
+
return node.tag + (node.interactive ? "!" : "");
|
|
449
|
+
}
|
|
450
|
+
const childSigs = node.children.map((ci) => {
|
|
451
|
+
const child = nodes[ci];
|
|
452
|
+
const childInteractive = child.interactive ? "!" : "";
|
|
453
|
+
if (child.children.length === 0) {
|
|
454
|
+
return child.tag + childInteractive;
|
|
455
|
+
}
|
|
456
|
+
const grandchildSigs = child.children.map((gi) => {
|
|
457
|
+
const gc = nodes[gi];
|
|
458
|
+
return gc.tag + (gc.interactive ? "!" : "");
|
|
459
|
+
}).join(",");
|
|
460
|
+
return child.tag + childInteractive + "(" + grandchildSigs + ")";
|
|
461
|
+
}).join(",");
|
|
462
|
+
return node.tag + ":[" + childSigs + "]";
|
|
463
|
+
}
|
|
464
|
+
__name(computeDepth2Signature, "computeDepth2Signature");
|
|
465
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
466
|
+
nodes[i].depth2Signature = computeDepth2Signature(i);
|
|
467
|
+
}
|
|
468
|
+
const detectedCollections = [];
|
|
469
|
+
let totalFuzzyMatched = 0;
|
|
470
|
+
function jaccardMultiset(a, b) {
|
|
471
|
+
const countA = /* @__PURE__ */ new Map();
|
|
472
|
+
const countB = /* @__PURE__ */ new Map();
|
|
473
|
+
for (const x of a) countA.set(x, (countA.get(x) || 0) + 1);
|
|
474
|
+
for (const x of b) countB.set(x, (countB.get(x) || 0) + 1);
|
|
475
|
+
const allKeys = /* @__PURE__ */ new Set([...countA.keys(), ...countB.keys()]);
|
|
476
|
+
let intersection = 0;
|
|
477
|
+
let union = 0;
|
|
478
|
+
for (const k of allKeys) {
|
|
479
|
+
const ca = countA.get(k) || 0;
|
|
480
|
+
const cb = countB.get(k) || 0;
|
|
481
|
+
intersection += Math.min(ca, cb);
|
|
482
|
+
union += Math.max(ca, cb);
|
|
483
|
+
}
|
|
484
|
+
return union === 0 ? 0 : intersection / union;
|
|
485
|
+
}
|
|
486
|
+
__name(jaccardMultiset, "jaccardMultiset");
|
|
487
|
+
function interactiveOverlap(dominantChildren, candidateChildren) {
|
|
488
|
+
const dominantInteractive = dominantChildren.filter((ci) => nodes[ci].interactive).map((ci) => nodes[ci].tag);
|
|
489
|
+
const candidateInteractive = candidateChildren.filter((ci) => nodes[ci].interactive).map((ci) => nodes[ci].tag);
|
|
490
|
+
if (dominantInteractive.length === 0) return 1;
|
|
491
|
+
let matched = 0;
|
|
492
|
+
const used = /* @__PURE__ */ new Set();
|
|
493
|
+
for (const dt of dominantInteractive) {
|
|
494
|
+
for (let i = 0; i < candidateInteractive.length; i++) {
|
|
495
|
+
if (!used.has(i) && candidateInteractive[i] === dt) {
|
|
496
|
+
matched++;
|
|
497
|
+
used.add(i);
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return matched / dominantInteractive.length;
|
|
503
|
+
}
|
|
504
|
+
__name(interactiveOverlap, "interactiveOverlap");
|
|
505
|
+
const collectionMemberSet = /* @__PURE__ */ new Set();
|
|
506
|
+
function addDescendantsToSet(nodeIdx, set) {
|
|
507
|
+
for (const ci of nodes[nodeIdx].children) {
|
|
508
|
+
set.add(ci);
|
|
509
|
+
addDescendantsToSet(ci, set);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
__name(addDescendantsToSet, "addDescendantsToSet");
|
|
513
|
+
function countInteractiveInSubtree(nodeIdx) {
|
|
514
|
+
let count = 0;
|
|
515
|
+
const n = nodes[nodeIdx];
|
|
516
|
+
if (n.interactive) count++;
|
|
517
|
+
for (const ci of n.children) {
|
|
518
|
+
count += countInteractiveInSubtree(ci);
|
|
519
|
+
}
|
|
520
|
+
return count;
|
|
521
|
+
}
|
|
522
|
+
__name(countInteractiveInSubtree, "countInteractiveInSubtree");
|
|
523
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
524
|
+
const node = nodes[i];
|
|
525
|
+
if (node.children.length < 3) continue;
|
|
526
|
+
if (collectionMemberSet.has(i)) continue;
|
|
527
|
+
const sigGroups = /* @__PURE__ */ new Map();
|
|
528
|
+
for (const ci of node.children) {
|
|
529
|
+
const sig = nodes[ci].depth2Signature || "";
|
|
530
|
+
if (!sigGroups.has(sig)) sigGroups.set(sig, []);
|
|
531
|
+
sigGroups.get(sig).push(ci);
|
|
532
|
+
}
|
|
533
|
+
let dominantSig = "";
|
|
534
|
+
let dominantGroup = [];
|
|
535
|
+
for (const [sig, group] of sigGroups) {
|
|
536
|
+
if (group.length > dominantGroup.length) {
|
|
537
|
+
dominantSig = sig;
|
|
538
|
+
dominantGroup = group;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (dominantGroup.length < 3) continue;
|
|
542
|
+
const dominantTag = nodes[dominantGroup[0]].tag;
|
|
543
|
+
const dominantChildTags = nodes[dominantGroup[0]].children.map((ci) => nodes[ci].tag);
|
|
544
|
+
let fuzzyCount = 0;
|
|
545
|
+
const allItems = [...dominantGroup];
|
|
546
|
+
for (const [sig, group] of sigGroups) {
|
|
547
|
+
if (sig === dominantSig) continue;
|
|
548
|
+
for (const ci of group) {
|
|
549
|
+
const candidate = nodes[ci];
|
|
550
|
+
if (candidate.tag !== dominantTag) continue;
|
|
551
|
+
const candidateChildTags = candidate.children.map((gi) => nodes[gi].tag);
|
|
552
|
+
const jaccard = jaccardMultiset(dominantChildTags, candidateChildTags);
|
|
553
|
+
if (jaccard < 0.7) continue;
|
|
554
|
+
const iOverlap = interactiveOverlap(nodes[dominantGroup[0]].children, candidate.children);
|
|
555
|
+
if (iOverlap < 0.5) continue;
|
|
556
|
+
allItems.push(ci);
|
|
557
|
+
fuzzyCount++;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (allItems.length < 3) continue;
|
|
561
|
+
const avgInteractive = allItems.reduce((sum, ci) => sum + countInteractiveInSubtree(ci), 0) / allItems.length;
|
|
562
|
+
if (avgInteractive < 3) continue;
|
|
563
|
+
detectedCollections.push({
|
|
564
|
+
parentIdx: i,
|
|
565
|
+
itemIndices: allItems,
|
|
566
|
+
dominantSignature: dominantSig,
|
|
567
|
+
fuzzyCount
|
|
568
|
+
});
|
|
569
|
+
totalFuzzyMatched += fuzzyCount;
|
|
570
|
+
for (const ci of allItems) {
|
|
571
|
+
collectionMemberSet.add(ci);
|
|
572
|
+
addDescendantsToSet(ci, collectionMemberSet);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
576
|
+
const node = nodes[i];
|
|
577
|
+
if (node.children.length < 3) continue;
|
|
578
|
+
if (node.children.some((ci) => collectionMemberSet.has(ci))) continue;
|
|
579
|
+
const grandchildIndices = [];
|
|
580
|
+
let valid = true;
|
|
581
|
+
for (const ci of node.children) {
|
|
582
|
+
const child = nodes[ci];
|
|
583
|
+
if (child.children.length !== 1) {
|
|
584
|
+
valid = false;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
grandchildIndices.push(child.children[0]);
|
|
588
|
+
}
|
|
589
|
+
if (!valid || grandchildIndices.length < 3) continue;
|
|
590
|
+
const sigGroups = /* @__PURE__ */ new Map();
|
|
591
|
+
for (const gi of grandchildIndices) {
|
|
592
|
+
const sig = nodes[gi].depth2Signature || "";
|
|
593
|
+
if (!sigGroups.has(sig)) sigGroups.set(sig, []);
|
|
594
|
+
sigGroups.get(sig).push(gi);
|
|
595
|
+
}
|
|
596
|
+
let dominantSig = "";
|
|
597
|
+
let dominantGroup = [];
|
|
598
|
+
for (const [sig, group] of sigGroups) {
|
|
599
|
+
if (group.length > dominantGroup.length) {
|
|
600
|
+
dominantSig = sig;
|
|
601
|
+
dominantGroup = group;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (dominantGroup.length < 3) continue;
|
|
605
|
+
const dominantTag = nodes[dominantGroup[0]].tag;
|
|
606
|
+
const dominantChildTags = nodes[dominantGroup[0]].children.map((ci) => nodes[ci].tag);
|
|
607
|
+
let fuzzyCount = 0;
|
|
608
|
+
const allItems = [...dominantGroup];
|
|
609
|
+
for (const [sig, group] of sigGroups) {
|
|
610
|
+
if (sig === dominantSig) continue;
|
|
611
|
+
for (const gi of group) {
|
|
612
|
+
const candidate = nodes[gi];
|
|
613
|
+
if (candidate.tag !== dominantTag) continue;
|
|
614
|
+
const candidateChildTags = candidate.children.map((ci) => nodes[ci].tag);
|
|
615
|
+
const jaccard = jaccardMultiset(dominantChildTags, candidateChildTags);
|
|
616
|
+
if (jaccard < 0.7) continue;
|
|
617
|
+
const iOverlap = interactiveOverlap(nodes[dominantGroup[0]].children, candidate.children);
|
|
618
|
+
if (iOverlap < 0.5) continue;
|
|
619
|
+
allItems.push(gi);
|
|
620
|
+
fuzzyCount++;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (allItems.length < 3) continue;
|
|
624
|
+
const avgInteractive = allItems.reduce((sum, ci) => sum + countInteractiveInSubtree(ci), 0) / allItems.length;
|
|
625
|
+
if (avgInteractive < 3) continue;
|
|
626
|
+
detectedCollections.push({
|
|
627
|
+
parentIdx: i,
|
|
628
|
+
itemIndices: allItems,
|
|
629
|
+
dominantSignature: dominantSig,
|
|
630
|
+
fuzzyCount
|
|
631
|
+
});
|
|
632
|
+
totalFuzzyMatched += fuzzyCount;
|
|
633
|
+
for (const ci of allItems) {
|
|
634
|
+
collectionMemberSet.add(ci);
|
|
635
|
+
addDescendantsToSet(ci, collectionMemberSet);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const LANDMARK_SELECTOR = 'header, nav, main, aside, footer, form, section[aria-label], section[aria-labelledby], article, dialog, [role="banner"], [role="navigation"], [role="main"], [role="contentinfo"], [role="search"], [role="region"], [role="complementary"], [role="dialog"], [role="alertdialog"]';
|
|
639
|
+
function landmarkKind(landmark) {
|
|
640
|
+
const tag = landmark.tagName.toLowerCase();
|
|
641
|
+
const role = (landmark.getAttribute("role") || "").toLowerCase();
|
|
642
|
+
if (tag === "nav" || role === "navigation") return "navigation";
|
|
643
|
+
if (tag === "form") return "form";
|
|
644
|
+
if (role === "search") return "search";
|
|
645
|
+
if (tag === "header" || role === "banner") return "header";
|
|
646
|
+
if (tag === "footer" || role === "contentinfo") return "footer";
|
|
647
|
+
if (tag === "main" || role === "main") return "main";
|
|
648
|
+
if (tag === "aside" || role === "complementary") return "aside";
|
|
649
|
+
if (tag === "dialog" || role === "dialog" || role === "alertdialog") return "dialog";
|
|
650
|
+
return "unknown";
|
|
651
|
+
}
|
|
652
|
+
__name(landmarkKind, "landmarkKind");
|
|
653
|
+
function landmarkLabel(landmark, kind) {
|
|
654
|
+
const tag = landmark.tagName.toLowerCase();
|
|
655
|
+
const role = landmark.getAttribute("role");
|
|
656
|
+
const ariaLabel = landmark.getAttribute("aria-label");
|
|
657
|
+
const heading = landmark.querySelector("h1, h2, h3, h4, h5, h6");
|
|
658
|
+
const headingText = heading?.textContent?.trim().slice(0, 40) || "";
|
|
659
|
+
const label = ariaLabel?.slice(0, 40) || headingText || "";
|
|
660
|
+
const baseName = role || tag;
|
|
661
|
+
const zoneKey = label ? `${baseName}: ${label}` : `${kind}:${baseName}`;
|
|
662
|
+
return { label, zoneKey };
|
|
663
|
+
}
|
|
664
|
+
__name(landmarkLabel, "landmarkLabel");
|
|
665
|
+
const syntheticRoots = /* @__PURE__ */ new Map();
|
|
666
|
+
for (const sel of CMP_SELECTORS) {
|
|
667
|
+
let matches = [];
|
|
668
|
+
try {
|
|
669
|
+
matches = Array.from(document.querySelectorAll(sel));
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
672
|
+
for (const el of matches) {
|
|
673
|
+
if (!isVisible(el)) continue;
|
|
674
|
+
if (!syntheticRoots.has(el)) {
|
|
675
|
+
syntheticRoots.set(el, { element: el, kind: "cmp", label: "consent" });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const overlayCandidates = Array.from(document.querySelectorAll("*"));
|
|
680
|
+
let overlayInspectBudget = 200;
|
|
681
|
+
for (const el of overlayCandidates) {
|
|
682
|
+
if (overlayInspectBudget-- <= 0) break;
|
|
683
|
+
if (!(el instanceof Element)) continue;
|
|
684
|
+
if (!isVisible(el)) continue;
|
|
685
|
+
if (syntheticRoots.has(el)) continue;
|
|
686
|
+
const style = el.style;
|
|
687
|
+
const pos = style && style.position;
|
|
688
|
+
if (pos !== "fixed" && pos !== "absolute") {
|
|
689
|
+
const computed = window.getComputedStyle(el);
|
|
690
|
+
if (computed.position !== "fixed" && computed.position !== "absolute") continue;
|
|
691
|
+
const zi = parseInt(computed.zIndex, 10);
|
|
692
|
+
if (!Number.isFinite(zi) || zi < 1e3) continue;
|
|
693
|
+
} else {
|
|
694
|
+
const computed = window.getComputedStyle(el);
|
|
695
|
+
const zi = parseInt(computed.zIndex, 10);
|
|
696
|
+
if (!Number.isFinite(zi) || zi < 1e3) continue;
|
|
697
|
+
}
|
|
698
|
+
if (!hasInteractiveDescendant(el)) continue;
|
|
699
|
+
if (el.closest(LANDMARK_SELECTOR) && el.closest(LANDMARK_SELECTOR) !== el) continue;
|
|
700
|
+
syntheticRoots.set(el, { element: el, kind: "overlay" });
|
|
701
|
+
}
|
|
702
|
+
function detectSyntheticByCollection() {
|
|
703
|
+
for (let ci = 0; ci < detectedCollections.length; ci++) {
|
|
704
|
+
const coll = detectedCollections[ci];
|
|
705
|
+
const parent = nodes[coll.parentIdx]?.element;
|
|
706
|
+
if (!parent) continue;
|
|
707
|
+
if (syntheticRoots.has(parent)) continue;
|
|
708
|
+
let productLinkCount = 0;
|
|
709
|
+
let promoCtaCount = 0;
|
|
710
|
+
let chipCount = 0;
|
|
711
|
+
for (const itemIdx of coll.itemIndices) {
|
|
712
|
+
const itemEl = nodes[itemIdx]?.element;
|
|
713
|
+
if (!itemEl) continue;
|
|
714
|
+
if (itemEl.querySelector('a[href*="/p/"], a[href*="/product/"], a[href*="/item/"]')) productLinkCount++;
|
|
715
|
+
if (itemEl.querySelector('[aria-label*="ktivierung" i], [aria-label*="Activate" i]')) promoCtaCount++;
|
|
716
|
+
const text2 = (itemEl.textContent || "").trim();
|
|
717
|
+
if (text2.length > 0 && text2.length <= 30) chipCount++;
|
|
718
|
+
}
|
|
719
|
+
if (productLinkCount >= 5) {
|
|
720
|
+
syntheticRoots.set(parent, { element: parent, kind: "recommendations" });
|
|
721
|
+
} else if (promoCtaCount >= 2) {
|
|
722
|
+
syntheticRoots.set(parent, { element: parent, kind: "promo" });
|
|
723
|
+
} else if (chipCount >= 5 && coll.itemIndices.length >= 5) {
|
|
724
|
+
syntheticRoots.set(parent, { element: parent, kind: "categoryChips" });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
__name(detectSyntheticByCollection, "detectSyntheticByCollection");
|
|
729
|
+
detectSyntheticByCollection();
|
|
730
|
+
function getZoneAssignment(el) {
|
|
731
|
+
let cursor = el;
|
|
732
|
+
while (cursor) {
|
|
733
|
+
const synth = syntheticRoots.get(cursor);
|
|
734
|
+
if (synth) {
|
|
735
|
+
const label = synth.label || "";
|
|
736
|
+
const zoneKey = label ? `${synth.kind}: ${label}` : synth.kind;
|
|
737
|
+
return { kind: synth.kind, zoneKey, label };
|
|
738
|
+
}
|
|
739
|
+
cursor = cursor.parentElement;
|
|
740
|
+
}
|
|
741
|
+
const landmark = el.closest(LANDMARK_SELECTOR);
|
|
742
|
+
if (landmark) {
|
|
743
|
+
const kind = landmarkKind(landmark);
|
|
744
|
+
const { label, zoneKey } = landmarkLabel(landmark, kind);
|
|
745
|
+
return { kind, zoneKey, label };
|
|
746
|
+
}
|
|
747
|
+
return { kind: "unknown", zoneKey: "unknown", label: "" };
|
|
748
|
+
}
|
|
749
|
+
__name(getZoneAssignment, "getZoneAssignment");
|
|
750
|
+
const standaloneInteractive = [];
|
|
751
|
+
const collectionItemInteractive = /* @__PURE__ */ new Map();
|
|
752
|
+
function isExportable(n) {
|
|
753
|
+
if (n.suppressedByHost) return false;
|
|
754
|
+
return n.interactive || n.exportAsActionableHost;
|
|
755
|
+
}
|
|
756
|
+
__name(isExportable, "isExportable");
|
|
757
|
+
function collectInteractiveFromSubtree(nodeIdx) {
|
|
758
|
+
const result = [];
|
|
759
|
+
const node = nodes[nodeIdx];
|
|
760
|
+
if (isExportable(node)) result.push(node);
|
|
761
|
+
for (const ci of node.children) {
|
|
762
|
+
result.push(...collectInteractiveFromSubtree(ci));
|
|
763
|
+
}
|
|
764
|
+
return result;
|
|
765
|
+
}
|
|
766
|
+
__name(collectInteractiveFromSubtree, "collectInteractiveFromSubtree");
|
|
767
|
+
const interactiveInCollection = /* @__PURE__ */ new Set();
|
|
768
|
+
for (let ci = 0; ci < detectedCollections.length; ci++) {
|
|
769
|
+
const coll = detectedCollections[ci];
|
|
770
|
+
const collInteractive = [];
|
|
771
|
+
for (const itemIdx of coll.itemIndices) {
|
|
772
|
+
const interactives = collectInteractiveFromSubtree(itemIdx);
|
|
773
|
+
collInteractive.push(...interactives);
|
|
774
|
+
for (const n of interactives) {
|
|
775
|
+
const idx = elementToIndex.get(n.element);
|
|
776
|
+
if (idx !== void 0) interactiveInCollection.add(idx);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
collectionItemInteractive.set(ci, collInteractive);
|
|
780
|
+
}
|
|
781
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
782
|
+
if (isExportable(nodes[i]) && !interactiveInCollection.has(i)) {
|
|
783
|
+
standaloneInteractive.push(nodes[i]);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const zoneMeta = /* @__PURE__ */ new Map();
|
|
787
|
+
const zoneElements = /* @__PURE__ */ new Map();
|
|
788
|
+
const zoneCollections = /* @__PURE__ */ new Map();
|
|
789
|
+
function registerZone(zoneKey, kind, label) {
|
|
790
|
+
if (zoneMeta.has(zoneKey)) return;
|
|
791
|
+
zoneMeta.set(zoneKey, label ? { kind, label } : { kind });
|
|
792
|
+
}
|
|
793
|
+
__name(registerZone, "registerZone");
|
|
794
|
+
for (const node of standaloneInteractive) {
|
|
795
|
+
const { kind, zoneKey, label } = getZoneAssignment(node.element);
|
|
796
|
+
registerZone(zoneKey, kind, label);
|
|
797
|
+
if (!zoneElements.has(zoneKey)) zoneElements.set(zoneKey, []);
|
|
798
|
+
zoneElements.get(zoneKey).push(node);
|
|
799
|
+
}
|
|
800
|
+
for (let ci = 0; ci < detectedCollections.length; ci++) {
|
|
801
|
+
const coll = detectedCollections[ci];
|
|
802
|
+
const parentNode = nodes[coll.parentIdx];
|
|
803
|
+
const { kind, zoneKey, label } = getZoneAssignment(parentNode.element);
|
|
804
|
+
registerZone(zoneKey, kind, label);
|
|
805
|
+
if (!zoneCollections.has(zoneKey)) zoneCollections.set(zoneKey, []);
|
|
806
|
+
zoneCollections.get(zoneKey).push(ci);
|
|
807
|
+
}
|
|
808
|
+
const tablesByZone = /* @__PURE__ */ new Map();
|
|
809
|
+
const tableEls = Array.from(document.querySelectorAll('table, [role="grid"], [role="table"], [role="treegrid"]'));
|
|
810
|
+
for (const tbl of tableEls) {
|
|
811
|
+
const rect = tbl.getBoundingClientRect();
|
|
812
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
813
|
+
const { kind, zoneKey, label } = getZoneAssignment(tbl);
|
|
814
|
+
registerZone(zoneKey, kind, label);
|
|
815
|
+
let headers = Array.from(tbl.querySelectorAll("thead th, thead td")).map((h) => (h.textContent || "").trim().slice(0, 40)).filter((t) => t);
|
|
816
|
+
if (headers.length === 0) {
|
|
817
|
+
const firstRow2 = tbl.querySelector("tr");
|
|
818
|
+
if (firstRow2) {
|
|
819
|
+
headers = Array.from(firstRow2.querySelectorAll("th")).map((h) => (h.textContent || "").trim().slice(0, 40)).filter((t) => t);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const bodyRows = tbl.querySelectorAll("tbody tr").length;
|
|
823
|
+
const totalRows = tbl.querySelectorAll("tr").length;
|
|
824
|
+
const rowCount = bodyRows || Math.max(0, totalRows - (headers.length > 0 ? 1 : 0));
|
|
825
|
+
const firstRow = tbl.querySelector("tr");
|
|
826
|
+
const columnCount = firstRow ? firstRow.children.length : headers.length;
|
|
827
|
+
const summary = {
|
|
828
|
+
selector: buildSelector(tbl),
|
|
829
|
+
columnCount,
|
|
830
|
+
rowCount,
|
|
831
|
+
headers: headers.slice(0, 20)
|
|
832
|
+
};
|
|
833
|
+
if (!tablesByZone.has(zoneKey)) tablesByZone.set(zoneKey, []);
|
|
834
|
+
tablesByZone.get(zoneKey).push(summary);
|
|
835
|
+
}
|
|
836
|
+
const allZoneNames = /* @__PURE__ */ new Set([...zoneElements.keys(), ...zoneCollections.keys(), ...tablesByZone.keys()]);
|
|
837
|
+
function extractTemplate(collIdx) {
|
|
838
|
+
const coll = detectedCollections[collIdx];
|
|
839
|
+
const items = coll.itemIndices;
|
|
840
|
+
const itemFields = [];
|
|
841
|
+
for (const itemIdx of items) {
|
|
842
|
+
const fields = [];
|
|
843
|
+
const stack = [itemIdx];
|
|
844
|
+
while (stack.length > 0) {
|
|
845
|
+
const ni = stack.pop();
|
|
846
|
+
const n = nodes[ni];
|
|
847
|
+
if (n.suppressedByHost) {
|
|
848
|
+
for (const ci of n.children) stack.push(ci);
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const effectiveInteractive = n.interactive || n.exportAsActionableHost;
|
|
852
|
+
if (effectiveInteractive || n.testId || n.qa || n.ariaLabel || n.role) {
|
|
853
|
+
fields.push({
|
|
854
|
+
tag: n.tag,
|
|
855
|
+
selector: buildSelector(n.element, n.shadowHost),
|
|
856
|
+
text: n.text,
|
|
857
|
+
ariaLabel: n.ariaLabel,
|
|
858
|
+
role: n.role,
|
|
859
|
+
type: n.type,
|
|
860
|
+
interactive: effectiveInteractive
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
for (const ci of n.children) {
|
|
864
|
+
stack.push(ci);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
itemFields.push(fields);
|
|
868
|
+
}
|
|
869
|
+
const templateMap = /* @__PURE__ */ new Map();
|
|
870
|
+
function normalizeSelectorForGrouping(sel) {
|
|
871
|
+
if (sel.startsWith("[data-testid=")) return sel;
|
|
872
|
+
if (sel.startsWith("[data-qa=")) return sel;
|
|
873
|
+
if (sel.startsWith("#")) return sel;
|
|
874
|
+
const hrefMatch = sel.match(/^a\[href[*=]?="(\/[^/]*\/)/);
|
|
875
|
+
if (hrefMatch) return `a[href*="${hrefMatch[1]}"]`;
|
|
876
|
+
const attrMatch = sel.match(/^(\w+)\[(\w+[-\w]*)=/);
|
|
877
|
+
if (attrMatch) return `${attrMatch[1]}[${attrMatch[2]}]`;
|
|
878
|
+
return sel.match(/^(\w+)/)?.[1] || sel;
|
|
879
|
+
}
|
|
880
|
+
__name(normalizeSelectorForGrouping, "normalizeSelectorForGrouping");
|
|
881
|
+
for (let itemI = 0; itemI < itemFields.length; itemI++) {
|
|
882
|
+
const seenInItem = /* @__PURE__ */ new Set();
|
|
883
|
+
for (const field of itemFields[itemI]) {
|
|
884
|
+
const key = `${field.tag}:${normalizeSelectorForGrouping(field.selector)}`;
|
|
885
|
+
if (seenInItem.has(key)) continue;
|
|
886
|
+
seenInItem.add(key);
|
|
887
|
+
if (!templateMap.has(key)) {
|
|
888
|
+
templateMap.set(key, {
|
|
889
|
+
tag: field.tag,
|
|
890
|
+
selectorPattern: field.selector,
|
|
891
|
+
texts: [],
|
|
892
|
+
ariaLabels: [],
|
|
893
|
+
roles: /* @__PURE__ */ new Set(),
|
|
894
|
+
types: /* @__PURE__ */ new Set(),
|
|
895
|
+
interactive: field.interactive,
|
|
896
|
+
count: 0
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
const tc = templateMap.get(key);
|
|
900
|
+
tc.count++;
|
|
901
|
+
if (field.text && tc.texts.length < 2 && !tc.texts.includes(field.text)) {
|
|
902
|
+
tc.texts.push(field.text);
|
|
903
|
+
}
|
|
904
|
+
if (field.ariaLabel && tc.ariaLabels.length < 2 && !tc.ariaLabels.includes(field.ariaLabel)) {
|
|
905
|
+
tc.ariaLabels.push(field.ariaLabel);
|
|
906
|
+
}
|
|
907
|
+
if (field.role) tc.roles.add(field.role);
|
|
908
|
+
if (field.type) tc.types.add(field.type);
|
|
909
|
+
if (field.selector.length > tc.selectorPattern.length) {
|
|
910
|
+
tc.selectorPattern = field.selector;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const template = [];
|
|
915
|
+
for (const [, tc] of templateMap) {
|
|
916
|
+
const optional = tc.count < items.length;
|
|
917
|
+
const consistentText = tc.texts.length === 1 && tc.count > 1 ? tc.texts[0] : void 0;
|
|
918
|
+
const sampleValues = tc.texts.length >= 1 && !consistentText ? tc.texts.slice(0, 2).map((t) => t.slice(0, 50)) : void 0;
|
|
919
|
+
const field = {
|
|
920
|
+
tag: tc.tag,
|
|
921
|
+
interactive: tc.interactive,
|
|
922
|
+
optional
|
|
923
|
+
};
|
|
924
|
+
if (tc.selectorPattern && tc.selectorPattern !== tc.tag) {
|
|
925
|
+
field.selector = tc.selectorPattern;
|
|
926
|
+
}
|
|
927
|
+
if (consistentText) field.text = consistentText.slice(0, 50);
|
|
928
|
+
if (tc.ariaLabels.length > 0) field.ariaLabel = tc.ariaLabels[0].slice(0, 50);
|
|
929
|
+
if (tc.roles.size === 1) field.role = [...tc.roles][0];
|
|
930
|
+
if (tc.types.size === 1) field.type = [...tc.types][0];
|
|
931
|
+
if (sampleValues && sampleValues.length > 0) field.sampleValues = sampleValues;
|
|
932
|
+
template.push(field);
|
|
933
|
+
}
|
|
934
|
+
template.sort((a, b) => {
|
|
935
|
+
if (a.interactive !== b.interactive) return a.interactive ? -1 : 1;
|
|
936
|
+
return a.tag.localeCompare(b.tag);
|
|
937
|
+
});
|
|
938
|
+
const MAX_ITEM_SAMPLES = 5;
|
|
939
|
+
const itemTexts = [];
|
|
940
|
+
const sampleItems = items.slice(0, MAX_ITEM_SAMPLES);
|
|
941
|
+
for (const itemIdx of sampleItems) {
|
|
942
|
+
let bestText = "";
|
|
943
|
+
const stack = [itemIdx];
|
|
944
|
+
while (stack.length > 0) {
|
|
945
|
+
const ni = stack.pop();
|
|
946
|
+
const n = nodes[ni];
|
|
947
|
+
if (n.interactive || n.exportAsActionableHost) {
|
|
948
|
+
const el = n.element;
|
|
949
|
+
const t = (el.getAttribute("title") || el.textContent || "").trim();
|
|
950
|
+
if (t.length > bestText.length) {
|
|
951
|
+
bestText = t.slice(0, 80);
|
|
952
|
+
if (el.getAttribute("title")) break;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
for (const ci of n.children) stack.push(ci);
|
|
956
|
+
}
|
|
957
|
+
if (!bestText) {
|
|
958
|
+
bestText = (nodes[itemIdx].element.textContent || "").trim().slice(0, 80);
|
|
959
|
+
}
|
|
960
|
+
itemTexts.push(bestText);
|
|
961
|
+
}
|
|
962
|
+
const result = { count: items.length, template };
|
|
963
|
+
if (itemTexts.some((t) => t.length > 0)) {
|
|
964
|
+
result.itemTexts = itemTexts;
|
|
965
|
+
}
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
968
|
+
__name(extractTemplate, "extractTemplate");
|
|
969
|
+
const collections = [];
|
|
970
|
+
for (let ci = 0; ci < detectedCollections.length; ci++) {
|
|
971
|
+
collections.push(extractTemplate(ci));
|
|
972
|
+
}
|
|
973
|
+
const textCounts = /* @__PURE__ */ new Map();
|
|
974
|
+
const selectorCounts = /* @__PURE__ */ new Map();
|
|
975
|
+
const inputTypeCounts = /* @__PURE__ */ new Map();
|
|
976
|
+
for (const node of standaloneInteractive) {
|
|
977
|
+
const tag = node.tag;
|
|
978
|
+
if (node.text) {
|
|
979
|
+
const key = `${tag}:${node.text.slice(0, 40)}`;
|
|
980
|
+
textCounts.set(key, (textCounts.get(key) || 0) + 1);
|
|
981
|
+
}
|
|
982
|
+
if (node.testId) {
|
|
983
|
+
const key = `testid:${node.testId}`;
|
|
984
|
+
selectorCounts.set(key, (selectorCounts.get(key) || 0) + 1);
|
|
985
|
+
}
|
|
986
|
+
if (node.qa) {
|
|
987
|
+
const key = `qa:${node.qa}`;
|
|
988
|
+
selectorCounts.set(key, (selectorCounts.get(key) || 0) + 1);
|
|
989
|
+
}
|
|
990
|
+
if (tag === "input") {
|
|
991
|
+
const key = `input:${node.type || "text"}`;
|
|
992
|
+
inputTypeCounts.set(key, (inputTypeCounts.get(key) || 0) + 1);
|
|
993
|
+
} else if (tag === "select" || tag === "textarea") {
|
|
994
|
+
const key = `input:${tag}`;
|
|
995
|
+
inputTypeCounts.set(key, (inputTypeCounts.get(key) || 0) + 1);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function getImportanceTier(node) {
|
|
999
|
+
const tag = node.tag;
|
|
1000
|
+
if (tag === "input" || tag === "select" || tag === "textarea") {
|
|
1001
|
+
const type = tag === "input" ? node.type || "text" : tag;
|
|
1002
|
+
const typeCount = inputTypeCounts.get(`input:${type}`) || 1;
|
|
1003
|
+
if (typeCount >= 10) return 3;
|
|
1004
|
+
if (typeCount >= 4) return 2;
|
|
1005
|
+
return 1;
|
|
1006
|
+
}
|
|
1007
|
+
if (node.testId) {
|
|
1008
|
+
const count2 = selectorCounts.get(`testid:${node.testId}`) || 1;
|
|
1009
|
+
if (count2 >= 5) return 3;
|
|
1010
|
+
if (count2 >= 2) return 2;
|
|
1011
|
+
return 1;
|
|
1012
|
+
}
|
|
1013
|
+
if (node.qa) {
|
|
1014
|
+
const count2 = selectorCounts.get(`qa:${node.qa}`) || 1;
|
|
1015
|
+
if (count2 >= 5) return 3;
|
|
1016
|
+
if (count2 >= 2) return 2;
|
|
1017
|
+
return 1;
|
|
1018
|
+
}
|
|
1019
|
+
if (node.id) return 1;
|
|
1020
|
+
const text2 = node.text?.slice(0, 40) || "";
|
|
1021
|
+
const key = `${tag}:${text2}`;
|
|
1022
|
+
const count = textCounts.get(key) || 1;
|
|
1023
|
+
if (count === 1 && text2 && (tag === "button" || node.role === "button")) return 1;
|
|
1024
|
+
if (count >= 5) return 3;
|
|
1025
|
+
if (count >= 2) return 2;
|
|
1026
|
+
return 2;
|
|
1027
|
+
}
|
|
1028
|
+
__name(getImportanceTier, "getImportanceTier");
|
|
1029
|
+
const ANCHOR_LANDMARK_TAGS = /* @__PURE__ */ new Set(["nav", "header", "footer", "main", "aside", "dialog", "form"]);
|
|
1030
|
+
const PW_PSEUDO_RE = /:has-text\(|\s>>\s|\brole=|\btext=/;
|
|
1031
|
+
function domUniquenessCheck(selector) {
|
|
1032
|
+
if (!selector || PW_PSEUDO_RE.test(selector)) return -1;
|
|
1033
|
+
try {
|
|
1034
|
+
return document.querySelectorAll(selector).length;
|
|
1035
|
+
} catch {
|
|
1036
|
+
return -1;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
__name(domUniquenessCheck, "domUniquenessCheck");
|
|
1040
|
+
function buildAnchorSelector(el) {
|
|
1041
|
+
const id = el.id;
|
|
1042
|
+
if (id && !isDynamicId(id)) return `#${id}`;
|
|
1043
|
+
const testId = el.getAttribute("data-testid");
|
|
1044
|
+
if (testId) return `[data-testid="${testId}"]`;
|
|
1045
|
+
const qa = el.getAttribute("data-qa");
|
|
1046
|
+
if (qa) return `[data-qa="${qa}"]`;
|
|
1047
|
+
const tag = el.tagName.toLowerCase();
|
|
1048
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
1049
|
+
if (ariaLabel) return `${tag}[aria-label="${ariaLabel.slice(0, 60)}"]`;
|
|
1050
|
+
if (ANCHOR_LANDMARK_TAGS.has(tag)) return tag;
|
|
1051
|
+
const explicitRole = el.getAttribute("role");
|
|
1052
|
+
if (explicitRole && (explicitRole === "navigation" || explicitRole === "banner" || explicitRole === "contentinfo" || explicitRole === "main" || explicitRole === "complementary" || explicitRole === "dialog" || explicitRole === "form" || explicitRole === "region")) {
|
|
1053
|
+
return `${tag}[role="${explicitRole}"]`;
|
|
1054
|
+
}
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
__name(buildAnchorSelector, "buildAnchorSelector");
|
|
1058
|
+
function ancestorAnchorRefine(el, initialSelector) {
|
|
1059
|
+
const isPwPseudo = PW_PSEUDO_RE.test(initialSelector);
|
|
1060
|
+
let cursor = el.parentElement;
|
|
1061
|
+
const body2 = document.body;
|
|
1062
|
+
while (cursor && cursor !== body2) {
|
|
1063
|
+
const anchorSel = buildAnchorSelector(cursor);
|
|
1064
|
+
if (anchorSel) {
|
|
1065
|
+
if (isPwPseudo) {
|
|
1066
|
+
const anchorCount = domUniquenessCheck(anchorSel);
|
|
1067
|
+
if (anchorCount !== 1) continue;
|
|
1068
|
+
const htMatch = initialSelector.match(/:has-text\("([^"]+)"\)/);
|
|
1069
|
+
if (htMatch) {
|
|
1070
|
+
const searchText = htMatch[1];
|
|
1071
|
+
const tagMatch = initialSelector.match(/^(\w+)/);
|
|
1072
|
+
const tag = tagMatch ? tagMatch[1] : "*";
|
|
1073
|
+
const descendants = cursor.querySelectorAll(tag);
|
|
1074
|
+
let textMatches = 0;
|
|
1075
|
+
for (let di = 0; di < descendants.length; di++) {
|
|
1076
|
+
if ((descendants[di].textContent || "").includes(searchText)) textMatches++;
|
|
1077
|
+
}
|
|
1078
|
+
if (textMatches === 1) {
|
|
1079
|
+
const candidate = `${anchorSel} ${initialSelector}`;
|
|
1080
|
+
return { selector: candidate, uniqueCount: 1, strategyApplied: "ancestor-anchor", ambiguous: false };
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
} else {
|
|
1084
|
+
const candidate = `${anchorSel} ${initialSelector}`;
|
|
1085
|
+
const count = domUniquenessCheck(candidate);
|
|
1086
|
+
if (count === 1) {
|
|
1087
|
+
return { selector: candidate, uniqueCount: 1, strategyApplied: "ancestor-anchor", ambiguous: false };
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
cursor = cursor.parentElement;
|
|
1092
|
+
}
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
__name(ancestorAnchorRefine, "ancestorAnchorRefine");
|
|
1096
|
+
function hasTextRefine(el, initialSelector) {
|
|
1097
|
+
if (/:has-text\(/.test(initialSelector)) return null;
|
|
1098
|
+
const chunk = shortTextChunk(el.textContent || "");
|
|
1099
|
+
if (!chunk) return null;
|
|
1100
|
+
let nodes2;
|
|
1101
|
+
try {
|
|
1102
|
+
nodes2 = Array.from(document.querySelectorAll(initialSelector));
|
|
1103
|
+
} catch {
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
const matches = nodes2.filter((n) => (n.textContent || "").includes(chunk));
|
|
1107
|
+
if (matches.length !== 1) return null;
|
|
1108
|
+
const escapedChunk = chunk.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1109
|
+
const refined = `${initialSelector}:has-text("${escapedChunk}")`;
|
|
1110
|
+
return { selector: refined, uniqueCount: 1, strategyApplied: "has-text", ambiguous: false };
|
|
1111
|
+
}
|
|
1112
|
+
__name(hasTextRefine, "hasTextRefine");
|
|
1113
|
+
function isDeepVisible(n) {
|
|
1114
|
+
if (!isVisible(n)) return false;
|
|
1115
|
+
try {
|
|
1116
|
+
const cs = window.getComputedStyle(n);
|
|
1117
|
+
if (cs.visibility === "hidden" || cs.visibility === "collapse") return false;
|
|
1118
|
+
if (parseFloat(cs.opacity) === 0) return false;
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
if (n.getAttribute("aria-hidden") === "true") return false;
|
|
1122
|
+
let parent = n.parentElement;
|
|
1123
|
+
let depth = 0;
|
|
1124
|
+
while (parent && depth < 10) {
|
|
1125
|
+
try {
|
|
1126
|
+
const ps = window.getComputedStyle(parent);
|
|
1127
|
+
if (ps.visibility === "hidden" || ps.visibility === "collapse") return false;
|
|
1128
|
+
if (parseFloat(ps.opacity) === 0) return false;
|
|
1129
|
+
} catch {
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
if (parent.getAttribute("aria-hidden") === "true") return false;
|
|
1133
|
+
parent = parent.parentElement;
|
|
1134
|
+
depth++;
|
|
1135
|
+
}
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
__name(isDeepVisible, "isDeepVisible");
|
|
1139
|
+
function visibilityRefine(el, initialSelector, initialCount) {
|
|
1140
|
+
if (initialCount <= 1) return null;
|
|
1141
|
+
let nodes2;
|
|
1142
|
+
try {
|
|
1143
|
+
nodes2 = Array.from(document.querySelectorAll(initialSelector));
|
|
1144
|
+
} catch {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
const visible = nodes2.filter((n) => isDeepVisible(n));
|
|
1148
|
+
if (visible.length === 1 && visible[0] === el) {
|
|
1149
|
+
return { selector: initialSelector, uniqueCount: 1, ambiguous: false };
|
|
1150
|
+
}
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
__name(visibilityRefine, "visibilityRefine");
|
|
1154
|
+
function slotRefine(el, initialSelector, initialCount) {
|
|
1155
|
+
if (initialCount <= 1) return null;
|
|
1156
|
+
if (PW_PSEUDO_RE.test(initialSelector)) return null;
|
|
1157
|
+
let nodes2;
|
|
1158
|
+
try {
|
|
1159
|
+
nodes2 = Array.from(document.querySelectorAll(initialSelector));
|
|
1160
|
+
} catch {
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
const elHasSlot = el.hasAttribute("slot");
|
|
1164
|
+
const others = nodes2.filter((n) => n !== el);
|
|
1165
|
+
const othersAllDifferent = others.every((n) => n.hasAttribute("slot") !== elHasSlot);
|
|
1166
|
+
if (!othersAllDifferent) return null;
|
|
1167
|
+
const suffix = elHasSlot ? "[slot]" : ":not([slot])";
|
|
1168
|
+
const refined = `${initialSelector}${suffix}`;
|
|
1169
|
+
const count = domUniquenessCheck(refined);
|
|
1170
|
+
if (count === 1) {
|
|
1171
|
+
return { selector: refined, uniqueCount: 1, strategyApplied: "slot-filter", ambiguous: false };
|
|
1172
|
+
}
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
__name(slotRefine, "slotRefine");
|
|
1176
|
+
function disambiguateSelector(el, initialSelector) {
|
|
1177
|
+
const initialCount = domUniquenessCheck(initialSelector);
|
|
1178
|
+
if (initialCount === 1) {
|
|
1179
|
+
return { selector: initialSelector, uniqueCount: 1, ambiguous: false };
|
|
1180
|
+
}
|
|
1181
|
+
if (initialCount <= 0) {
|
|
1182
|
+
return { selector: initialSelector, uniqueCount: initialCount, ambiguous: initialCount !== 1 };
|
|
1183
|
+
}
|
|
1184
|
+
const s1 = ancestorAnchorRefine(el, initialSelector);
|
|
1185
|
+
if (s1) return s1;
|
|
1186
|
+
const s2 = hasTextRefine(el, initialSelector);
|
|
1187
|
+
if (s2) return s2;
|
|
1188
|
+
const s4 = slotRefine(el, initialSelector, initialCount);
|
|
1189
|
+
if (s4) return s4;
|
|
1190
|
+
const s3 = visibilityRefine(el, initialSelector, initialCount);
|
|
1191
|
+
if (s3) return s3;
|
|
1192
|
+
return { selector: initialSelector, uniqueCount: initialCount, ambiguous: true };
|
|
1193
|
+
}
|
|
1194
|
+
__name(disambiguateSelector, "disambiguateSelector");
|
|
1195
|
+
function nodeToAction(node) {
|
|
1196
|
+
const action = {};
|
|
1197
|
+
action.tag = node.tag;
|
|
1198
|
+
const { selector, stability } = buildSelectorWithStability(node.element, node.shadowHost);
|
|
1199
|
+
action.selector = selector;
|
|
1200
|
+
action.stability = stability;
|
|
1201
|
+
if (node.text) action.text = node.text;
|
|
1202
|
+
if (node.qa) action.qa = node.qa;
|
|
1203
|
+
if (node.testId) action.testId = node.testId;
|
|
1204
|
+
if (node.role) action.role = node.role;
|
|
1205
|
+
if (node.tag === "a" && node.href) action.href = node.href;
|
|
1206
|
+
if (node.tag === "input") action.type = node.type || "text";
|
|
1207
|
+
try {
|
|
1208
|
+
const rect = node.element.getBoundingClientRect();
|
|
1209
|
+
const x = Math.round(rect.x);
|
|
1210
|
+
const y = Math.round(rect.y);
|
|
1211
|
+
const w = Math.round(rect.width);
|
|
1212
|
+
const h = Math.round(rect.height);
|
|
1213
|
+
if (w > 0 || h > 0) {
|
|
1214
|
+
action.bounds = { x, y, w, h };
|
|
1215
|
+
const vw = window.innerWidth;
|
|
1216
|
+
const vh = window.innerHeight;
|
|
1217
|
+
if (x < 0 && x + w > 0 || y < 0 && y + h > 0 || x < vw && x + w > vw || y < vh && y + h > vh) {
|
|
1218
|
+
action.clipped = true;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
} catch {
|
|
1222
|
+
}
|
|
1223
|
+
if (disambiguateEnabled && action.selector) {
|
|
1224
|
+
const result = disambiguateSelector(node.element, action.selector);
|
|
1225
|
+
action.selector = result.selector;
|
|
1226
|
+
if (result.strategyApplied) action.disambiguatedBy = result.strategyApplied;
|
|
1227
|
+
if (result.ambiguous) action.ambiguous = true;
|
|
1228
|
+
}
|
|
1229
|
+
return action;
|
|
1230
|
+
}
|
|
1231
|
+
__name(nodeToAction, "nodeToAction");
|
|
1232
|
+
function groupLinksByHrefPattern(linkNodes) {
|
|
1233
|
+
const pathGroups = /* @__PURE__ */ new Map();
|
|
1234
|
+
const ungrouped = [];
|
|
1235
|
+
for (const node of linkNodes) {
|
|
1236
|
+
if (!node.href) {
|
|
1237
|
+
ungrouped.push(node);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
const segments = node.href.split("/").filter(Boolean);
|
|
1241
|
+
const patternKey = segments.length >= 1 ? `/${segments[0]}/` : null;
|
|
1242
|
+
if (patternKey) {
|
|
1243
|
+
if (!pathGroups.has(patternKey)) pathGroups.set(patternKey, []);
|
|
1244
|
+
pathGroups.get(patternKey).push(node);
|
|
1245
|
+
} else {
|
|
1246
|
+
ungrouped.push(node);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1250
|
+
for (const [key, ns] of pathGroups) {
|
|
1251
|
+
if (ns.length >= 3) {
|
|
1252
|
+
grouped.set(key, ns);
|
|
1253
|
+
} else {
|
|
1254
|
+
ungrouped.push(...ns);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return { grouped, ungrouped };
|
|
1258
|
+
}
|
|
1259
|
+
__name(groupLinksByHrefPattern, "groupLinksByHrefPattern");
|
|
1260
|
+
function groupByTestId(nodeList) {
|
|
1261
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1262
|
+
const ungrouped = [];
|
|
1263
|
+
for (const node of nodeList) {
|
|
1264
|
+
const key = node.testId ? `testid:${node.testId}` : node.qa ? `qa:${node.qa}` : null;
|
|
1265
|
+
if (key) {
|
|
1266
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
1267
|
+
groups.get(key).push(node);
|
|
1268
|
+
} else {
|
|
1269
|
+
ungrouped.push(node);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
const validGroups = /* @__PURE__ */ new Map();
|
|
1273
|
+
for (const [key, ns] of groups) {
|
|
1274
|
+
if (ns.length >= 3) {
|
|
1275
|
+
validGroups.set(key, ns);
|
|
1276
|
+
} else {
|
|
1277
|
+
ungrouped.push(...ns);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return { groups: validGroups, ungrouped };
|
|
1281
|
+
}
|
|
1282
|
+
__name(groupByTestId, "groupByTestId");
|
|
1283
|
+
function groupByTagWithType(nodeList) {
|
|
1284
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1285
|
+
const ungrouped = [];
|
|
1286
|
+
for (const node of nodeList) {
|
|
1287
|
+
if (node.tag === "input") {
|
|
1288
|
+
const key = `input:${node.type || "text"}`;
|
|
1289
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
1290
|
+
groups.get(key).push(node);
|
|
1291
|
+
} else if (node.tag.includes("-")) {
|
|
1292
|
+
if (!groups.has(node.tag)) groups.set(node.tag, []);
|
|
1293
|
+
groups.get(node.tag).push(node);
|
|
1294
|
+
} else {
|
|
1295
|
+
ungrouped.push(node);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const validGroups = /* @__PURE__ */ new Map();
|
|
1299
|
+
for (const [key, ns] of groups) {
|
|
1300
|
+
if (ns.length >= 5) {
|
|
1301
|
+
validGroups.set(key, ns);
|
|
1302
|
+
} else {
|
|
1303
|
+
ungrouped.push(...ns);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return { groups: validGroups, ungrouped };
|
|
1307
|
+
}
|
|
1308
|
+
__name(groupByTagWithType, "groupByTagWithType");
|
|
1309
|
+
const TIER2_CAP_PER_ZONE = 15;
|
|
1310
|
+
const regions = {};
|
|
1311
|
+
const textLines = [];
|
|
1312
|
+
for (const zoneName of allZoneNames) {
|
|
1313
|
+
const zoneNodes = zoneElements.get(zoneName) || [];
|
|
1314
|
+
const zoneCollIndices = zoneCollections.get(zoneName) || [];
|
|
1315
|
+
const meta = zoneMeta.get(zoneName) || { kind: "unknown" };
|
|
1316
|
+
const kind = meta.kind;
|
|
1317
|
+
let totalInZone = zoneNodes.length;
|
|
1318
|
+
for (const ci of zoneCollIndices) {
|
|
1319
|
+
totalInZone += (collectionItemInteractive.get(ci) || []).length;
|
|
1320
|
+
}
|
|
1321
|
+
const isNavigation = kind === "navigation" || kind === "header";
|
|
1322
|
+
const bulkTestIdNodes = /* @__PURE__ */ new Set();
|
|
1323
|
+
const bulkTestIdPatterns = [];
|
|
1324
|
+
const commonHrefPrefix = /* @__PURE__ */ __name((ns) => {
|
|
1325
|
+
const hrefs = ns.map((n) => n.href || "").filter((h) => h);
|
|
1326
|
+
if (hrefs.length / ns.length < 0.8) return null;
|
|
1327
|
+
const paths = hrefs.map((h) => h.split(/[?#]/)[0].split("/").filter(Boolean));
|
|
1328
|
+
if (!paths.length) return null;
|
|
1329
|
+
let k = 0;
|
|
1330
|
+
const first = paths[0];
|
|
1331
|
+
for (let i = 0; i < first.length; i++) {
|
|
1332
|
+
if (paths.every((p) => p[i] === first[i])) k = i + 1;
|
|
1333
|
+
else break;
|
|
1334
|
+
}
|
|
1335
|
+
if (k === 0) return null;
|
|
1336
|
+
return "/" + first.slice(0, k).join("/") + "/";
|
|
1337
|
+
}, "commonHrefPrefix");
|
|
1338
|
+
const buildTestIdSelector = /* @__PURE__ */ __name((ns, tidPart) => {
|
|
1339
|
+
const hrefPrefix = commonHrefPrefix(ns);
|
|
1340
|
+
return hrefPrefix ? `a[href*="${hrefPrefix}"]${tidPart}` : tidPart;
|
|
1341
|
+
}, "buildTestIdSelector");
|
|
1342
|
+
if (!isNavigation) {
|
|
1343
|
+
const byExact = /* @__PURE__ */ new Map();
|
|
1344
|
+
for (const n of zoneNodes) {
|
|
1345
|
+
const tid = n.testId || n.qa;
|
|
1346
|
+
if (tid) {
|
|
1347
|
+
if (!byExact.has(tid)) byExact.set(tid, []);
|
|
1348
|
+
byExact.get(tid).push(n);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
const remainingForPrefix = [];
|
|
1352
|
+
for (const [tid, ns] of byExact) {
|
|
1353
|
+
if (ns.length >= 3) {
|
|
1354
|
+
const attr = ns[0].qa ? "data-qa" : "data-testid";
|
|
1355
|
+
const selector = buildTestIdSelector(ns, `[${attr}="${tid}"]`);
|
|
1356
|
+
bulkTestIdPatterns.push({
|
|
1357
|
+
pattern: selector,
|
|
1358
|
+
selectorPattern: selector,
|
|
1359
|
+
count: ns.length,
|
|
1360
|
+
samples: pickPatternSamples(ns, ns.length)
|
|
1361
|
+
});
|
|
1362
|
+
for (const n of ns) bulkTestIdNodes.add(n);
|
|
1363
|
+
} else {
|
|
1364
|
+
for (const n of ns) remainingForPrefix.push(n);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
const segOf = /* @__PURE__ */ __name((id) => id.split(/[-_/]/).filter((s) => s.length > 0), "segOf");
|
|
1368
|
+
const usedByPrefix = /* @__PURE__ */ new Set();
|
|
1369
|
+
for (let k = 3; k >= 1; k--) {
|
|
1370
|
+
const byPrefix = /* @__PURE__ */ new Map();
|
|
1371
|
+
for (const n of remainingForPrefix) {
|
|
1372
|
+
if (usedByPrefix.has(n)) continue;
|
|
1373
|
+
const tid = n.testId || n.qa || "";
|
|
1374
|
+
const segs = segOf(tid);
|
|
1375
|
+
if (segs.length < k + 1) continue;
|
|
1376
|
+
const prefix = segs.slice(0, k).join("-");
|
|
1377
|
+
if (!byPrefix.has(prefix)) byPrefix.set(prefix, []);
|
|
1378
|
+
byPrefix.get(prefix).push(n);
|
|
1379
|
+
}
|
|
1380
|
+
for (const [prefix, ns] of byPrefix) {
|
|
1381
|
+
if (ns.length >= 3) {
|
|
1382
|
+
const attr = ns[0].qa ? "data-qa" : "data-testid";
|
|
1383
|
+
const selector = buildTestIdSelector(ns, `[${attr}^="${prefix}"]`);
|
|
1384
|
+
bulkTestIdPatterns.push({
|
|
1385
|
+
pattern: selector,
|
|
1386
|
+
selectorPattern: selector,
|
|
1387
|
+
count: ns.length,
|
|
1388
|
+
samples: pickPatternSamples(ns, ns.length)
|
|
1389
|
+
});
|
|
1390
|
+
for (const n of ns) {
|
|
1391
|
+
bulkTestIdNodes.add(n);
|
|
1392
|
+
usedByPrefix.add(n);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
const tier1 = [];
|
|
1399
|
+
const tier2Nodes = [];
|
|
1400
|
+
const tier3 = [];
|
|
1401
|
+
const links = [];
|
|
1402
|
+
for (const node of zoneNodes) {
|
|
1403
|
+
if (bulkTestIdNodes.has(node)) continue;
|
|
1404
|
+
if (isNavigation) {
|
|
1405
|
+
tier1.push(nodeToAction(node));
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
const tier = getImportanceTier(node);
|
|
1409
|
+
if (tier === 1) {
|
|
1410
|
+
tier1.push(nodeToAction(node));
|
|
1411
|
+
} else if (node.tag === "a") {
|
|
1412
|
+
links.push(node);
|
|
1413
|
+
} else if (tier === 2) {
|
|
1414
|
+
tier2Nodes.push(node);
|
|
1415
|
+
} else {
|
|
1416
|
+
tier3.push(node);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
const patterns = [...bulkTestIdPatterns];
|
|
1420
|
+
const { grouped: hrefGroups, ungrouped: ungroupedLinks } = groupLinksByHrefPattern(links);
|
|
1421
|
+
for (const node of ungroupedLinks) {
|
|
1422
|
+
const tier = getImportanceTier(node);
|
|
1423
|
+
if (tier === 1) tier1.push(nodeToAction(node));
|
|
1424
|
+
else if (tier === 2) tier2Nodes.push(node);
|
|
1425
|
+
else tier3.push(node);
|
|
1426
|
+
}
|
|
1427
|
+
for (const [pathPrefix, ns] of hrefGroups) {
|
|
1428
|
+
patterns.push({
|
|
1429
|
+
pattern: `a${pathPrefix}*`,
|
|
1430
|
+
selectorPattern: `a[href*="${pathPrefix}"]`,
|
|
1431
|
+
count: ns.length,
|
|
1432
|
+
samples: pickPatternSamples(ns, ns.length)
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
const { groups: t2TagGroups, ungrouped: t2Ungrouped } = groupByTagWithType(tier2Nodes);
|
|
1436
|
+
for (const [tagKey, ns] of t2TagGroups) {
|
|
1437
|
+
const displayName = tagKey.includes(":") ? `${tagKey.split(":")[0]}[type=${tagKey.split(":")[1]}]` : tagKey;
|
|
1438
|
+
patterns.push({
|
|
1439
|
+
pattern: displayName,
|
|
1440
|
+
selectorPattern: tagKey.includes("-") ? tagKey : void 0,
|
|
1441
|
+
count: ns.length,
|
|
1442
|
+
samples: pickPatternSamples(ns, ns.length)
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
const cappedTier2 = t2Ungrouped.slice(0, TIER2_CAP_PER_ZONE).map(nodeToAction);
|
|
1446
|
+
const tier2Overflow = Math.max(0, t2Ungrouped.length - TIER2_CAP_PER_ZONE);
|
|
1447
|
+
const { groups: testidGroups, ungrouped: noTestidTier3 } = groupByTestId(tier3);
|
|
1448
|
+
for (const [key, ns] of testidGroups) {
|
|
1449
|
+
const isTestid = key.startsWith("testid:");
|
|
1450
|
+
const value = key.split(":").slice(1).join(":");
|
|
1451
|
+
const selectorStr = isTestid ? `[data-testid="${value}"]` : `[data-qa="${value}"]`;
|
|
1452
|
+
patterns.push({
|
|
1453
|
+
pattern: selectorStr,
|
|
1454
|
+
selectorPattern: selectorStr,
|
|
1455
|
+
count: ns.length,
|
|
1456
|
+
samples: pickPatternSamples(ns, ns.length)
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
const { groups: tier3TagGroups, ungrouped: ungroupedTier3 } = groupByTagWithType(noTestidTier3);
|
|
1460
|
+
for (const [tagKey, ns] of tier3TagGroups) {
|
|
1461
|
+
const displayName = tagKey.includes(":") ? `${tagKey.split(":")[0]}[type=${tagKey.split(":")[1]}]` : tagKey;
|
|
1462
|
+
patterns.push({
|
|
1463
|
+
pattern: displayName,
|
|
1464
|
+
selectorPattern: tagKey.includes("-") ? tagKey : void 0,
|
|
1465
|
+
count: ns.length,
|
|
1466
|
+
samples: pickPatternSamples(ns, ns.length)
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
const otherCount = ungroupedTier3.length + tier2Overflow;
|
|
1470
|
+
const actions = [...tier1, ...cappedTier2];
|
|
1471
|
+
const region = { kind, total: totalInZone };
|
|
1472
|
+
if (meta.label) region.label = meta.label;
|
|
1473
|
+
if (actions.length > 0) region.actions = actions;
|
|
1474
|
+
if (zoneCollIndices.length > 0) {
|
|
1475
|
+
region.collections = zoneCollIndices.map((ci) => collections[ci]);
|
|
1476
|
+
}
|
|
1477
|
+
if (patterns.length > 0) region.patterns = patterns;
|
|
1478
|
+
const zoneTables = tablesByZone.get(zoneName);
|
|
1479
|
+
if (zoneTables && zoneTables.length > 0) region.tables = zoneTables;
|
|
1480
|
+
if (otherCount > 0) region.other = otherCount;
|
|
1481
|
+
regions[zoneName] = region;
|
|
1482
|
+
textLines.push(`=== ${zoneName} [${kind}] (${totalInZone} els) ===`);
|
|
1483
|
+
for (const a of tier1) {
|
|
1484
|
+
const parts = [];
|
|
1485
|
+
const sel = stripRandomTail(a.selector);
|
|
1486
|
+
if (sel) parts.push(sel);
|
|
1487
|
+
if (a.text) parts.push(`"${a.text.slice(0, 40)}"`);
|
|
1488
|
+
if (a.qa && !(sel && sel.includes(`data-qa="${a.qa}"`))) parts.push(`[data-qa=${a.qa}]`);
|
|
1489
|
+
else if (a.testId && !(sel && sel.includes(`data-testid="${a.testId}"`))) parts.push(`[data-testid=${a.testId}]`);
|
|
1490
|
+
if (a.role) parts.push(`[${a.role}]`);
|
|
1491
|
+
if (a.type) parts.push(`type=${a.type}`);
|
|
1492
|
+
textLines.push(`* ${parts.join(" ")}`);
|
|
1493
|
+
}
|
|
1494
|
+
for (const a of cappedTier2) {
|
|
1495
|
+
const parts = [];
|
|
1496
|
+
const tag = a.tag || "";
|
|
1497
|
+
parts.push(tag);
|
|
1498
|
+
if (a.text) parts.push(`"${a.text.slice(0, 30)}"`);
|
|
1499
|
+
const sel = stripRandomTail(a.selector);
|
|
1500
|
+
if (sel && sel !== tag) parts.push(sel);
|
|
1501
|
+
textLines.push(`- ${parts.join(" ")}`);
|
|
1502
|
+
}
|
|
1503
|
+
for (const ci of zoneCollIndices) {
|
|
1504
|
+
const coll = collections[ci];
|
|
1505
|
+
textLines.push("");
|
|
1506
|
+
textLines.push(`collection(${coll.count} items, ${coll.template.length} fields):`);
|
|
1507
|
+
if (coll.itemTexts && coll.itemTexts.length > 0) {
|
|
1508
|
+
for (let ii = 0; ii < coll.itemTexts.length; ii++) {
|
|
1509
|
+
const t = coll.itemTexts[ii].length > 60 ? coll.itemTexts[ii].slice(0, 60) + "..." : coll.itemTexts[ii];
|
|
1510
|
+
textLines.push(` [${ii}] "${t}"`);
|
|
1511
|
+
}
|
|
1512
|
+
const remaining = coll.count - coll.itemTexts.length;
|
|
1513
|
+
if (remaining > 0) {
|
|
1514
|
+
textLines.push(` ...${remaining} more`);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
const fieldLines = [];
|
|
1518
|
+
for (const f of coll.template) {
|
|
1519
|
+
const parts = [];
|
|
1520
|
+
parts.push(f.tag);
|
|
1521
|
+
const sel = stripRandomTail(f.selector);
|
|
1522
|
+
if (sel) parts.push(sel);
|
|
1523
|
+
if (f.text) {
|
|
1524
|
+
parts.push(`"${f.text}"`);
|
|
1525
|
+
} else if (f.sampleValues && f.sampleValues.length > 0) {
|
|
1526
|
+
const samples = dedupStrings(f.sampleValues);
|
|
1527
|
+
parts.push(`\u2014 "${samples.join('", "')}"`);
|
|
1528
|
+
}
|
|
1529
|
+
if (f.ariaLabel) parts.push(`[aria-label="${f.ariaLabel}"]`);
|
|
1530
|
+
if (f.role) parts.push(`[${f.role}]`);
|
|
1531
|
+
if (f.type) parts.push(`type=${f.type}`);
|
|
1532
|
+
fieldLines.push(parts.join(" "));
|
|
1533
|
+
}
|
|
1534
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1535
|
+
const order = [];
|
|
1536
|
+
for (const line of fieldLines) {
|
|
1537
|
+
if (!groups.has(line)) order.push(line);
|
|
1538
|
+
groups.set(line, (groups.get(line) || 0) + 1);
|
|
1539
|
+
}
|
|
1540
|
+
for (const line of order) {
|
|
1541
|
+
const n = groups.get(line);
|
|
1542
|
+
const prefix = n > 1 ? ` - ${n}\xD7 ` : ` - `;
|
|
1543
|
+
textLines.push(prefix + line);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (zoneTables && zoneTables.length > 0) {
|
|
1547
|
+
for (const t of zoneTables) {
|
|
1548
|
+
const headersPart = t.headers.length ? ` \u2014 headers: [${t.headers.join(", ")}]` : "";
|
|
1549
|
+
textLines.push(`table(${t.rowCount}\xD7${t.columnCount}) ${t.selector}${headersPart}`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
for (const p of patterns) {
|
|
1553
|
+
const stripped = p.selectorPattern ? stripRandomTail(p.selectorPattern) : void 0;
|
|
1554
|
+
const head = stripped || p.pattern;
|
|
1555
|
+
const tag = p.pattern;
|
|
1556
|
+
const cleanSamples = p.samples ? dedupStrings(p.samples.filter((s) => !isNoiseSample(s, tag))) : [];
|
|
1557
|
+
if (p.count < 2 && cleanSamples.length === 0) continue;
|
|
1558
|
+
const samplesStr = cleanSamples.length ? ` ("${cleanSamples.join('", "')}")` : "";
|
|
1559
|
+
textLines.push(`~ ${p.count}x ${head}${samplesStr}`);
|
|
1560
|
+
}
|
|
1561
|
+
if (otherCount > 0) {
|
|
1562
|
+
textLines.push(`. ${otherCount} other`);
|
|
1563
|
+
}
|
|
1564
|
+
textLines.push("");
|
|
1565
|
+
}
|
|
1566
|
+
const KEEP_SMALL_KINDS = /* @__PURE__ */ new Set(["form", "search", "cmp", "dialog"]);
|
|
1567
|
+
const regionsToDrop = [];
|
|
1568
|
+
let unknownTotalActions = 0;
|
|
1569
|
+
let unknownRegionKey = null;
|
|
1570
|
+
for (const [zoneKey, region] of Object.entries(regions)) {
|
|
1571
|
+
if (region.kind === "unknown") {
|
|
1572
|
+
unknownRegionKey = zoneKey;
|
|
1573
|
+
unknownTotalActions += region.total;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
for (const [zoneKey, region] of Object.entries(regions)) {
|
|
1577
|
+
if (KEEP_SMALL_KINDS.has(region.kind)) continue;
|
|
1578
|
+
if (region.kind === "unknown") continue;
|
|
1579
|
+
if (region.total >= 3) continue;
|
|
1580
|
+
if (region.collections && region.collections.length > 0 || region.tables && region.tables.length > 0) continue;
|
|
1581
|
+
if (!unknownRegionKey) {
|
|
1582
|
+
unknownRegionKey = "unknown";
|
|
1583
|
+
regions[unknownRegionKey] = { kind: "unknown", total: 0 };
|
|
1584
|
+
}
|
|
1585
|
+
const target = regions[unknownRegionKey];
|
|
1586
|
+
if (region.actions) {
|
|
1587
|
+
target.actions = (target.actions || []).concat(region.actions);
|
|
1588
|
+
}
|
|
1589
|
+
if (region.patterns) {
|
|
1590
|
+
target.patterns = (target.patterns || []).concat(region.patterns);
|
|
1591
|
+
}
|
|
1592
|
+
target.total += region.total;
|
|
1593
|
+
regionsToDrop.push(zoneKey);
|
|
1594
|
+
}
|
|
1595
|
+
for (const k of regionsToDrop) delete regions[k];
|
|
1596
|
+
const warnings = [];
|
|
1597
|
+
const totalActions = Object.values(regions).reduce((s, r) => s + r.total, 0);
|
|
1598
|
+
if (totalActions > 0) {
|
|
1599
|
+
const unknownShare = unknownTotalActions / totalActions;
|
|
1600
|
+
if (unknownShare > 0.1) warnings.push(`unclassified-heavy: unknown region holds ${Math.round(unknownShare * 100)}% of elements`);
|
|
1601
|
+
}
|
|
1602
|
+
const text = textLines.join("\n");
|
|
1603
|
+
const chars = text.length;
|
|
1604
|
+
const tokens = Math.ceil(chars / 4);
|
|
1605
|
+
const rawText = document.body?.innerText?.slice(0, 2e4) || "";
|
|
1606
|
+
const totalDomElements = document.querySelectorAll("*").length;
|
|
1607
|
+
const body = document.body;
|
|
1608
|
+
const bodyVisible = body ? (() => {
|
|
1609
|
+
const r = body.getBoundingClientRect();
|
|
1610
|
+
return r.height > 0 && r.width > 0;
|
|
1611
|
+
})() : false;
|
|
1612
|
+
const bodyChildren = body?.children.length || 0;
|
|
1613
|
+
const pageTitle = document.title || "";
|
|
1614
|
+
return {
|
|
1615
|
+
text,
|
|
1616
|
+
rawText,
|
|
1617
|
+
regions,
|
|
1618
|
+
stats: {
|
|
1619
|
+
totalElements: totalInteractive,
|
|
1620
|
+
filteredElements: standaloneInteractive.length + interactiveInCollection.size,
|
|
1621
|
+
chars,
|
|
1622
|
+
tokens
|
|
1623
|
+
},
|
|
1624
|
+
diagnostics: {
|
|
1625
|
+
collections: detectedCollections.length,
|
|
1626
|
+
shadowHosts: shadowHostCount,
|
|
1627
|
+
shadowElements: shadowElementCount,
|
|
1628
|
+
fuzzyMatched: totalFuzzyMatched,
|
|
1629
|
+
totalDomElements,
|
|
1630
|
+
visibleDomElements: visibleCount,
|
|
1631
|
+
interestingElements: interestingCount,
|
|
1632
|
+
customElements: customElementCount,
|
|
1633
|
+
pageTitle,
|
|
1634
|
+
bodyChildren,
|
|
1635
|
+
bodyVisible,
|
|
1636
|
+
walkVisits,
|
|
1637
|
+
actionableHosts: actionableHostCount,
|
|
1638
|
+
suppressedByHost: suppressedByHostCount,
|
|
1639
|
+
warnings: warnings.length > 0 ? warnings : void 0
|
|
1640
|
+
},
|
|
1641
|
+
viewport: { width: window.innerWidth, height: window.innerHeight }
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
__name(semanticDomAlgorithm, "semanticDomAlgorithm");
|
|
1645
|
+
|
|
1646
|
+
// src/util/debug-log.ts
|
|
1647
|
+
import {
|
|
1648
|
+
appendFileSync as fsAppend,
|
|
1649
|
+
mkdirSync as fsMkdir,
|
|
1650
|
+
renameSync as fsRename,
|
|
1651
|
+
rmSync as fsRm,
|
|
1652
|
+
symlinkSync as fsSymlink,
|
|
1653
|
+
unlinkSync as fsUnlink,
|
|
1654
|
+
writeFileSync as fsWrite
|
|
1655
|
+
} from "fs";
|
|
1656
|
+
import { join } from "path";
|
|
1657
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
1658
|
+
var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
1659
|
+
var DEFAULT_HTML_MAX_BYTES = 64 * 1024;
|
|
1660
|
+
var debugStore = new AsyncLocalStorage();
|
|
1661
|
+
|
|
1662
|
+
// src/inspection/outline-capture.ts
|
|
1663
|
+
import { outlineAlgorithm as outlineAlgorithmInjectable } from "@unotest/web/internal/page-inject";
|
|
1664
|
+
function outlineAlgorithm(assignRefs) {
|
|
1665
|
+
const INTERACTIVE_ARIA_ROLES = /* @__PURE__ */ new Set([
|
|
1666
|
+
"button",
|
|
1667
|
+
"link",
|
|
1668
|
+
"checkbox",
|
|
1669
|
+
"radio",
|
|
1670
|
+
"menuitem",
|
|
1671
|
+
"menuitemcheckbox",
|
|
1672
|
+
"menuitemradio",
|
|
1673
|
+
"tab",
|
|
1674
|
+
"option",
|
|
1675
|
+
"switch",
|
|
1676
|
+
"combobox",
|
|
1677
|
+
"listbox",
|
|
1678
|
+
"slider",
|
|
1679
|
+
"spinbutton",
|
|
1680
|
+
"searchbox",
|
|
1681
|
+
"textbox"
|
|
1682
|
+
]);
|
|
1683
|
+
const LANDMARK_TAGS = {
|
|
1684
|
+
header: "banner",
|
|
1685
|
+
nav: "navigation",
|
|
1686
|
+
main: "main",
|
|
1687
|
+
aside: "complementary",
|
|
1688
|
+
footer: "contentinfo",
|
|
1689
|
+
form: "form",
|
|
1690
|
+
section: "region",
|
|
1691
|
+
// only when it has aria-label or aria-labelledby
|
|
1692
|
+
article: "article",
|
|
1693
|
+
dialog: "dialog"
|
|
1694
|
+
};
|
|
1695
|
+
const LANDMARK_ROLES = /* @__PURE__ */ new Set([
|
|
1696
|
+
"banner",
|
|
1697
|
+
"navigation",
|
|
1698
|
+
"main",
|
|
1699
|
+
"complementary",
|
|
1700
|
+
"contentinfo",
|
|
1701
|
+
"form",
|
|
1702
|
+
"search",
|
|
1703
|
+
"region",
|
|
1704
|
+
"dialog",
|
|
1705
|
+
"alertdialog",
|
|
1706
|
+
"article"
|
|
1707
|
+
]);
|
|
1708
|
+
const GENERIC_ROLES = /* @__PURE__ */ new Set(["generic", "none", "presentation"]);
|
|
1709
|
+
function* deepWalk(root) {
|
|
1710
|
+
const children = root.children;
|
|
1711
|
+
for (let i = 0; i < children.length; i++) {
|
|
1712
|
+
const el = children[i];
|
|
1713
|
+
yield el;
|
|
1714
|
+
const sr = el.shadowRoot;
|
|
1715
|
+
if (sr) yield* deepWalk(sr);
|
|
1716
|
+
yield* deepWalk(el);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
__name(deepWalk, "deepWalk");
|
|
1720
|
+
function parentOrHost(node) {
|
|
1721
|
+
const parent = node.parentNode;
|
|
1722
|
+
if (!parent) return null;
|
|
1723
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
1724
|
+
return parent instanceof Element ? parent : null;
|
|
1725
|
+
}
|
|
1726
|
+
__name(parentOrHost, "parentOrHost");
|
|
1727
|
+
function rootForScope(el) {
|
|
1728
|
+
const root = el.getRootNode();
|
|
1729
|
+
return root instanceof ShadowRoot ? root : document;
|
|
1730
|
+
}
|
|
1731
|
+
__name(rootForScope, "rootForScope");
|
|
1732
|
+
function getElementByIdInScope(el, id) {
|
|
1733
|
+
return rootForScope(el).getElementById(id);
|
|
1734
|
+
}
|
|
1735
|
+
__name(getElementByIdInScope, "getElementByIdInScope");
|
|
1736
|
+
function isVisible(el) {
|
|
1737
|
+
if (!el.getClientRects || el.getClientRects().length === 0) return false;
|
|
1738
|
+
const cs = window.getComputedStyle(el);
|
|
1739
|
+
if (cs.display === "none" || cs.visibility === "hidden") return false;
|
|
1740
|
+
if (Number.parseFloat(cs.opacity || "1") < 0.01) return false;
|
|
1741
|
+
for (let p = el; p; p = parentOrHost(p)) {
|
|
1742
|
+
if (p.getAttribute && p.getAttribute("aria-hidden") === "true") return false;
|
|
1743
|
+
}
|
|
1744
|
+
const r = el.getBoundingClientRect();
|
|
1745
|
+
if (r.width === 0 && r.height === 0) return false;
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1748
|
+
__name(isVisible, "isVisible");
|
|
1749
|
+
function implicitRole(el) {
|
|
1750
|
+
const tag = el.tagName.toLowerCase();
|
|
1751
|
+
if (tag === "a" && el.hasAttribute("href")) return "link";
|
|
1752
|
+
if (tag === "button") return "button";
|
|
1753
|
+
if (tag === "textarea") return "textbox";
|
|
1754
|
+
if (tag === "select") {
|
|
1755
|
+
return el.multiple || el.size > 1 ? "listbox" : "combobox";
|
|
1756
|
+
}
|
|
1757
|
+
if (tag === "input") {
|
|
1758
|
+
const t = (el.type || "text").toLowerCase();
|
|
1759
|
+
if (t === "button" || t === "submit" || t === "reset" || t === "image") return "button";
|
|
1760
|
+
if (t === "checkbox") return "checkbox";
|
|
1761
|
+
if (t === "radio") return "radio";
|
|
1762
|
+
if (t === "range") return "slider";
|
|
1763
|
+
if (t === "number") return "spinbutton";
|
|
1764
|
+
if (t === "search") return "searchbox";
|
|
1765
|
+
if (t === "hidden") return null;
|
|
1766
|
+
return "textbox";
|
|
1767
|
+
}
|
|
1768
|
+
if (tag === "h1" || tag === "h2" || tag === "h3" || tag === "h4" || tag === "h5" || tag === "h6") {
|
|
1769
|
+
return "heading";
|
|
1770
|
+
}
|
|
1771
|
+
if (tag === "img" && el.hasAttribute("alt")) return "img";
|
|
1772
|
+
if (tag === "details") return "group";
|
|
1773
|
+
if (tag === "summary") return "button";
|
|
1774
|
+
if (tag === "iframe") return "iframe";
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
__name(implicitRole, "implicitRole");
|
|
1778
|
+
function computeRole(el) {
|
|
1779
|
+
const explicit = el.getAttribute("role");
|
|
1780
|
+
if (explicit) {
|
|
1781
|
+
return explicit.split(/\s+/)[0].toLowerCase();
|
|
1782
|
+
}
|
|
1783
|
+
return implicitRole(el);
|
|
1784
|
+
}
|
|
1785
|
+
__name(computeRole, "computeRole");
|
|
1786
|
+
function isInteractive(el) {
|
|
1787
|
+
const tag = el.tagName.toLowerCase();
|
|
1788
|
+
if (tag === "iframe") return true;
|
|
1789
|
+
const role = computeRole(el);
|
|
1790
|
+
if (role && INTERACTIVE_ARIA_ROLES.has(role)) return true;
|
|
1791
|
+
if (el.hasAttribute("onclick")) return true;
|
|
1792
|
+
const tiRaw = el.getAttribute("tabindex");
|
|
1793
|
+
const ti = tiRaw === null ? null : Number.parseInt(tiRaw, 10);
|
|
1794
|
+
if (ti !== null && ti >= 0) return true;
|
|
1795
|
+
if (tag === "div" || tag === "span") {
|
|
1796
|
+
}
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
__name(isInteractive, "isInteractive");
|
|
1800
|
+
const MAX_NAME = 80;
|
|
1801
|
+
function trim(s) {
|
|
1802
|
+
return (s ?? "").replace(/\s+/g, " ").trim();
|
|
1803
|
+
}
|
|
1804
|
+
__name(trim, "trim");
|
|
1805
|
+
function cap(s) {
|
|
1806
|
+
return s.length > MAX_NAME ? s.slice(0, MAX_NAME - 1) + "\u2026" : s;
|
|
1807
|
+
}
|
|
1808
|
+
__name(cap, "cap");
|
|
1809
|
+
function svgIconName(host, seen) {
|
|
1810
|
+
const svg = host.querySelector("svg");
|
|
1811
|
+
if (!svg) return "";
|
|
1812
|
+
const labelledby = svg.getAttribute("aria-labelledby");
|
|
1813
|
+
if (labelledby) {
|
|
1814
|
+
const parts = [];
|
|
1815
|
+
for (const id of labelledby.split(/\s+/)) {
|
|
1816
|
+
if (!id) continue;
|
|
1817
|
+
const ref = getElementByIdInScope(svg, id);
|
|
1818
|
+
if (ref && !seen.has(ref)) {
|
|
1819
|
+
const n = accname(ref, seen);
|
|
1820
|
+
if (n) parts.push(n);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const joined = trim(parts.join(" "));
|
|
1824
|
+
if (joined) return cap(joined);
|
|
1825
|
+
}
|
|
1826
|
+
const ariaLabel = trim(svg.getAttribute("aria-label"));
|
|
1827
|
+
if (ariaLabel) return cap(ariaLabel);
|
|
1828
|
+
for (const child of Array.from(svg.children)) {
|
|
1829
|
+
if (child.tagName.toLowerCase() === "title") {
|
|
1830
|
+
const t = trim(child.textContent);
|
|
1831
|
+
if (t) return cap(t);
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return "";
|
|
1836
|
+
}
|
|
1837
|
+
__name(svgIconName, "svgIconName");
|
|
1838
|
+
function accname(el, visited) {
|
|
1839
|
+
const seen = visited ?? /* @__PURE__ */ new Set();
|
|
1840
|
+
if (seen.has(el)) return "";
|
|
1841
|
+
seen.add(el);
|
|
1842
|
+
const labelledby = el.getAttribute("aria-labelledby");
|
|
1843
|
+
if (labelledby) {
|
|
1844
|
+
const parts = [];
|
|
1845
|
+
for (const id of labelledby.split(/\s+/)) {
|
|
1846
|
+
if (!id) continue;
|
|
1847
|
+
const ref = getElementByIdInScope(el, id);
|
|
1848
|
+
if (ref && !seen.has(ref)) {
|
|
1849
|
+
const n = accname(ref, seen);
|
|
1850
|
+
if (n) parts.push(n);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
const joined = trim(parts.join(" "));
|
|
1854
|
+
if (joined) return cap(joined);
|
|
1855
|
+
}
|
|
1856
|
+
const ariaLabel = trim(el.getAttribute("aria-label"));
|
|
1857
|
+
if (ariaLabel) return cap(ariaLabel);
|
|
1858
|
+
const tag = el.tagName.toLowerCase();
|
|
1859
|
+
if (tag === "input" || tag === "textarea" || tag === "select") {
|
|
1860
|
+
const id = el.id;
|
|
1861
|
+
if (id) {
|
|
1862
|
+
const lbl = rootForScope(el).querySelector(
|
|
1863
|
+
`label[for="${cssEscape(id)}"]`
|
|
1864
|
+
);
|
|
1865
|
+
if (lbl) {
|
|
1866
|
+
const n = trim(textContentVisible(lbl));
|
|
1867
|
+
if (n) return cap(n);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
let p = parentOrHost(el);
|
|
1871
|
+
while (p) {
|
|
1872
|
+
if (p.tagName.toLowerCase() === "label") {
|
|
1873
|
+
const n = trim(textContentVisible(p));
|
|
1874
|
+
if (n) return cap(n);
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1877
|
+
p = parentOrHost(p);
|
|
1878
|
+
}
|
|
1879
|
+
if (tag === "input") {
|
|
1880
|
+
const it = el.type?.toLowerCase();
|
|
1881
|
+
if (it === "button" || it === "submit" || it === "reset") {
|
|
1882
|
+
const v = trim(el.value);
|
|
1883
|
+
if (v) return cap(v);
|
|
1884
|
+
}
|
|
1885
|
+
const ph = trim(el.placeholder);
|
|
1886
|
+
if (ph) return cap(ph);
|
|
1887
|
+
}
|
|
1888
|
+
const t2 = trim(el.getAttribute("title"));
|
|
1889
|
+
if (t2) return cap(t2);
|
|
1890
|
+
return "";
|
|
1891
|
+
}
|
|
1892
|
+
if (tag === "button" || tag === "a" || tag === "summary") {
|
|
1893
|
+
const txt = trim(textContentVisible(el));
|
|
1894
|
+
if (txt) return cap(txt);
|
|
1895
|
+
const svgName = svgIconName(el, seen);
|
|
1896
|
+
if (svgName) return svgName;
|
|
1897
|
+
const img = el.querySelector("img[alt]");
|
|
1898
|
+
if (img) {
|
|
1899
|
+
const alt = trim(img.getAttribute("alt"));
|
|
1900
|
+
if (alt) return cap(alt);
|
|
1901
|
+
}
|
|
1902
|
+
const t2 = trim(el.getAttribute("title"));
|
|
1903
|
+
if (t2) return cap(t2);
|
|
1904
|
+
return "";
|
|
1905
|
+
}
|
|
1906
|
+
if (tag === "iframe") {
|
|
1907
|
+
const t2 = trim(el.getAttribute("title"));
|
|
1908
|
+
if (t2) return cap(t2);
|
|
1909
|
+
const src = el.getAttribute("src");
|
|
1910
|
+
if (src) {
|
|
1911
|
+
const base = srcBasename(src);
|
|
1912
|
+
if (base) return cap(base);
|
|
1913
|
+
}
|
|
1914
|
+
return "";
|
|
1915
|
+
}
|
|
1916
|
+
if (tag === "img") {
|
|
1917
|
+
return cap(trim(el.getAttribute("alt")));
|
|
1918
|
+
}
|
|
1919
|
+
const inner = trim(textContentVisible(el));
|
|
1920
|
+
if (inner) return cap(inner);
|
|
1921
|
+
const t = trim(el.getAttribute("title"));
|
|
1922
|
+
return cap(t);
|
|
1923
|
+
}
|
|
1924
|
+
__name(accname, "accname");
|
|
1925
|
+
function srcBasename(src) {
|
|
1926
|
+
const noQuery = src.split(/[?#]/)[0] ?? "";
|
|
1927
|
+
const segments = noQuery.split("/").filter((s) => s.length > 0);
|
|
1928
|
+
return segments[segments.length - 1] ?? "";
|
|
1929
|
+
}
|
|
1930
|
+
__name(srcBasename, "srcBasename");
|
|
1931
|
+
function textContentVisible(el) {
|
|
1932
|
+
const ht = el;
|
|
1933
|
+
if (typeof ht.innerText === "string") return ht.innerText;
|
|
1934
|
+
return el.textContent ?? "";
|
|
1935
|
+
}
|
|
1936
|
+
__name(textContentVisible, "textContentVisible");
|
|
1937
|
+
function cssEscape(s) {
|
|
1938
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1939
|
+
}
|
|
1940
|
+
__name(cssEscape, "cssEscape");
|
|
1941
|
+
const MAX_HREF = 60;
|
|
1942
|
+
const pageOrigin = (() => {
|
|
1943
|
+
try {
|
|
1944
|
+
return window.location.origin;
|
|
1945
|
+
} catch {
|
|
1946
|
+
return "";
|
|
1947
|
+
}
|
|
1948
|
+
})();
|
|
1949
|
+
function shortenedHref(rawHref) {
|
|
1950
|
+
if (!rawHref) return "";
|
|
1951
|
+
const t = trim(rawHref);
|
|
1952
|
+
if (!t || t === "#") return "";
|
|
1953
|
+
try {
|
|
1954
|
+
const u = new URL(t, window.location.href);
|
|
1955
|
+
if (u.protocol === "javascript:" || u.protocol === "mailto:" || u.protocol === "tel:") {
|
|
1956
|
+
return "";
|
|
1957
|
+
}
|
|
1958
|
+
let display;
|
|
1959
|
+
if (pageOrigin && u.origin === pageOrigin) {
|
|
1960
|
+
display = u.pathname + u.search + u.hash;
|
|
1961
|
+
} else {
|
|
1962
|
+
display = u.host + u.pathname + u.search + u.hash;
|
|
1963
|
+
}
|
|
1964
|
+
if (display.length > MAX_HREF) display = display.slice(0, MAX_HREF - 1) + "\u2026";
|
|
1965
|
+
return display;
|
|
1966
|
+
} catch {
|
|
1967
|
+
return t.length > MAX_HREF ? t.slice(0, MAX_HREF - 1) + "\u2026" : t;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
__name(shortenedHref, "shortenedHref");
|
|
1971
|
+
function collectState(el) {
|
|
1972
|
+
const state = {};
|
|
1973
|
+
const tag = el.tagName.toLowerCase();
|
|
1974
|
+
const formDisabled = (tag === "button" || tag === "input" || tag === "select" || tag === "textarea" || tag === "fieldset" || tag === "optgroup" || tag === "option") && el.disabled === true;
|
|
1975
|
+
if (formDisabled || el.getAttribute("aria-disabled") === "true") {
|
|
1976
|
+
state.disabled = true;
|
|
1977
|
+
}
|
|
1978
|
+
const ariaChecked = el.getAttribute("aria-checked");
|
|
1979
|
+
if (ariaChecked === "true") {
|
|
1980
|
+
state.checked = true;
|
|
1981
|
+
} else if (ariaChecked === "mixed") {
|
|
1982
|
+
state.checked = "mixed";
|
|
1983
|
+
} else if (tag === "input" && (el.type === "checkbox" || el.type === "radio") && el.checked) {
|
|
1984
|
+
state.checked = true;
|
|
1985
|
+
}
|
|
1986
|
+
const ariaPressed = el.getAttribute("aria-pressed");
|
|
1987
|
+
if (ariaPressed === "true") state.pressed = true;
|
|
1988
|
+
else if (ariaPressed === "mixed") state.pressed = "mixed";
|
|
1989
|
+
if (el.getAttribute("aria-expanded") === "true") state.expanded = true;
|
|
1990
|
+
if (el.getAttribute("aria-selected") === "true") {
|
|
1991
|
+
state.selected = true;
|
|
1992
|
+
} else if (tag === "option" && el.selected) {
|
|
1993
|
+
state.selected = true;
|
|
1994
|
+
}
|
|
1995
|
+
if (rootForScope(el).activeElement === el) {
|
|
1996
|
+
state.active = true;
|
|
1997
|
+
}
|
|
1998
|
+
if (tag === "h1") state.level = 1;
|
|
1999
|
+
else if (tag === "h2") state.level = 2;
|
|
2000
|
+
else if (tag === "h3") state.level = 3;
|
|
2001
|
+
else if (tag === "h4") state.level = 4;
|
|
2002
|
+
else if (tag === "h5") state.level = 5;
|
|
2003
|
+
else if (tag === "h6") state.level = 6;
|
|
2004
|
+
const ariaLevel = el.getAttribute("aria-level");
|
|
2005
|
+
if (ariaLevel) {
|
|
2006
|
+
const n = Number.parseInt(ariaLevel, 10);
|
|
2007
|
+
if (Number.isFinite(n) && n > 0) state.level = n;
|
|
2008
|
+
}
|
|
2009
|
+
return Object.keys(state).length > 0 ? state : void 0;
|
|
2010
|
+
}
|
|
2011
|
+
__name(collectState, "collectState");
|
|
2012
|
+
function findLandmark(el) {
|
|
2013
|
+
for (let p = parentOrHost(el); p; p = parentOrHost(p)) {
|
|
2014
|
+
const role = p.getAttribute("role");
|
|
2015
|
+
if (role) {
|
|
2016
|
+
const r = role.split(/\s+/)[0].toLowerCase();
|
|
2017
|
+
if (LANDMARK_ROLES.has(r)) {
|
|
2018
|
+
return { kind: r, label: landmarkLabel(p), element: p };
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
const tag = p.tagName.toLowerCase();
|
|
2022
|
+
const tagKind = LANDMARK_TAGS[tag];
|
|
2023
|
+
if (tagKind) {
|
|
2024
|
+
if (tag === "section") {
|
|
2025
|
+
if (!p.getAttribute("aria-label") && !p.getAttribute("aria-labelledby")) continue;
|
|
2026
|
+
}
|
|
2027
|
+
return { kind: tagKind, label: landmarkLabel(p), element: p };
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
return null;
|
|
2031
|
+
}
|
|
2032
|
+
__name(findLandmark, "findLandmark");
|
|
2033
|
+
function landmarkLabel(landmark) {
|
|
2034
|
+
const aria = trim(landmark.getAttribute("aria-label"));
|
|
2035
|
+
if (aria) return cap(aria);
|
|
2036
|
+
const labelledby = landmark.getAttribute("aria-labelledby");
|
|
2037
|
+
if (labelledby) {
|
|
2038
|
+
const parts = [];
|
|
2039
|
+
for (const id of labelledby.split(/\s+/)) {
|
|
2040
|
+
if (!id) continue;
|
|
2041
|
+
const ref = getElementByIdInScope(landmark, id);
|
|
2042
|
+
if (ref) parts.push(trim(textContentVisible(ref)));
|
|
2043
|
+
}
|
|
2044
|
+
const joined = trim(parts.join(" "));
|
|
2045
|
+
if (joined) return cap(joined);
|
|
2046
|
+
}
|
|
2047
|
+
const heading = landmark.querySelector("h1, h2, h3, h4, h5, h6");
|
|
2048
|
+
if (heading) {
|
|
2049
|
+
const txt = trim(textContentVisible(heading));
|
|
2050
|
+
if (txt) return cap(txt);
|
|
2051
|
+
}
|
|
2052
|
+
return "";
|
|
2053
|
+
}
|
|
2054
|
+
__name(landmarkLabel, "landmarkLabel");
|
|
2055
|
+
function testIdOf(el) {
|
|
2056
|
+
const t = el.getAttribute("data-testid");
|
|
2057
|
+
if (t) return t;
|
|
2058
|
+
const qa = el.getAttribute("data-qa");
|
|
2059
|
+
if (qa) return qa;
|
|
2060
|
+
return void 0;
|
|
2061
|
+
}
|
|
2062
|
+
__name(testIdOf, "testIdOf");
|
|
2063
|
+
function idStable(id) {
|
|
2064
|
+
if (!id || id.length > 40) return false;
|
|
2065
|
+
if (id.startsWith(":") || id.startsWith("__")) return false;
|
|
2066
|
+
const digits = id.replace(/\D/g, "").length;
|
|
2067
|
+
return !(id.length > 8 && digits * 2 > id.length);
|
|
2068
|
+
}
|
|
2069
|
+
__name(idStable, "idStable");
|
|
2070
|
+
function stableDataAttr(el) {
|
|
2071
|
+
const skip = /^data-(unotest|v-|react|ng-|svelte|qa|testid|sort|shadow|tooltip|toggle|target|bs-|original-title)/;
|
|
2072
|
+
const idLike = /(^|[-_])(id|guid|uid|key|code|ref|num|no)$/;
|
|
2073
|
+
for (const a of Array.from(el.attributes)) {
|
|
2074
|
+
if (!a.name.startsWith("data-") || skip.test(a.name) || !idLike.test(a.name)) continue;
|
|
2075
|
+
const v = (a.value ?? "").trim();
|
|
2076
|
+
if (!v || v.length > 64) continue;
|
|
2077
|
+
if (/^\d+$/.test(v) || idStable(v)) return { name: a.name, value: v };
|
|
2078
|
+
}
|
|
2079
|
+
return void 0;
|
|
2080
|
+
}
|
|
2081
|
+
__name(stableDataAttr, "stableDataAttr");
|
|
2082
|
+
function tooltipLabel(el) {
|
|
2083
|
+
for (const a of Array.from(el.attributes)) {
|
|
2084
|
+
if (!a.name.startsWith("data-") || a.name.startsWith("data-unotest")) continue;
|
|
2085
|
+
if (!/(^|-)(title|tooltip|label)$/.test(a.name)) continue;
|
|
2086
|
+
const v = trim(a.value);
|
|
2087
|
+
if (v) return v.length > 120 ? v.slice(0, 119) + "\u2026" : v;
|
|
2088
|
+
}
|
|
2089
|
+
return void 0;
|
|
2090
|
+
}
|
|
2091
|
+
__name(tooltipLabel, "tooltipLabel");
|
|
2092
|
+
function tooltipText(el, name) {
|
|
2093
|
+
const t = trim(el.getAttribute("title") ?? el.getAttribute("aria-label")) || tooltipLabel(el);
|
|
2094
|
+
if (!t || t === name) return void 0;
|
|
2095
|
+
return t.length > 120 ? t.slice(0, 119) + "\u2026" : t;
|
|
2096
|
+
}
|
|
2097
|
+
__name(tooltipText, "tooltipText");
|
|
2098
|
+
function clickAffordance(el) {
|
|
2099
|
+
if (el.hasAttribute("onclick")) return true;
|
|
2100
|
+
const cls = el.getAttribute("class") ?? "";
|
|
2101
|
+
if (/(^|\s)hover:/.test(cls) || /\bcursor-pointer\b/.test(cls) || /\bclickable\b/.test(cls)) {
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
try {
|
|
2105
|
+
return window.getComputedStyle(el).cursor === "pointer";
|
|
2106
|
+
} catch {
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
__name(clickAffordance, "clickAffordance");
|
|
2111
|
+
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
2112
|
+
function classifyViewport(b) {
|
|
2113
|
+
if (viewport.width <= 0 || viewport.height <= 0) return void 0;
|
|
2114
|
+
const top = b.y;
|
|
2115
|
+
const bottom = b.y + b.h;
|
|
2116
|
+
const left = b.x;
|
|
2117
|
+
const right = b.x + b.w;
|
|
2118
|
+
const vTop = 0;
|
|
2119
|
+
const vBottom = viewport.height;
|
|
2120
|
+
const vLeft = 0;
|
|
2121
|
+
const vRight = viewport.width;
|
|
2122
|
+
if (bottom <= vTop) return "above";
|
|
2123
|
+
if (top >= vBottom) return "below";
|
|
2124
|
+
if (right <= vLeft || left >= vRight) {
|
|
2125
|
+
return "partial";
|
|
2126
|
+
}
|
|
2127
|
+
if (top >= vTop && bottom <= vBottom && left >= vLeft && right <= vRight) {
|
|
2128
|
+
return void 0;
|
|
2129
|
+
}
|
|
2130
|
+
return "partial";
|
|
2131
|
+
}
|
|
2132
|
+
__name(classifyViewport, "classifyViewport");
|
|
2133
|
+
function isClipped(b) {
|
|
2134
|
+
return b.x < 0 && b.x + b.width > 0 || b.y < 0 && b.y + b.height > 0 || b.x < viewport.width && b.x + b.width > viewport.width || b.y < viewport.height && b.y + b.height > viewport.height;
|
|
2135
|
+
}
|
|
2136
|
+
__name(isClipped, "isClipped");
|
|
2137
|
+
function rectToBox(r) {
|
|
2138
|
+
return { x: r.x, y: r.y, w: r.width, h: r.height };
|
|
2139
|
+
}
|
|
2140
|
+
__name(rectToBox, "rectToBox");
|
|
2141
|
+
function unionBox(a, b) {
|
|
2142
|
+
if (!a) return b;
|
|
2143
|
+
const x1 = Math.min(a.x, b.x);
|
|
2144
|
+
const y1 = Math.min(a.y, b.y);
|
|
2145
|
+
const x2 = Math.max(a.x + a.w, b.x + b.w);
|
|
2146
|
+
const y2 = Math.max(a.y + a.h, b.y + b.h);
|
|
2147
|
+
return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
|
|
2148
|
+
}
|
|
2149
|
+
__name(unionBox, "unionBox");
|
|
2150
|
+
function findTrAncestor(el) {
|
|
2151
|
+
for (let p = parentOrHost(el); p; p = parentOrHost(p)) {
|
|
2152
|
+
if (p.tagName.toLowerCase() === "tr") return p;
|
|
2153
|
+
}
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
__name(findTrAncestor, "findTrAncestor");
|
|
2157
|
+
const interactiveElements = [];
|
|
2158
|
+
const clickableEls = /* @__PURE__ */ new Set();
|
|
2159
|
+
for (const el of deepWalk(document)) {
|
|
2160
|
+
if (!isVisible(el)) continue;
|
|
2161
|
+
if (isInteractive(el)) {
|
|
2162
|
+
interactiveElements.push(el);
|
|
2163
|
+
continue;
|
|
2164
|
+
}
|
|
2165
|
+
const da = stableDataAttr(el);
|
|
2166
|
+
if (da && trim(textContentVisible(el)) && clickAffordance(el)) {
|
|
2167
|
+
interactiveElements.push(el);
|
|
2168
|
+
clickableEls.add(el);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
const refRegex = /^e\d+$/;
|
|
2172
|
+
const previouslyRefed = /* @__PURE__ */ new Map();
|
|
2173
|
+
for (const el of deepWalk(document)) {
|
|
2174
|
+
const v = el.getAttribute("data-unotest-ref");
|
|
2175
|
+
if (v && refRegex.test(v)) previouslyRefed.set(el, v);
|
|
2176
|
+
}
|
|
2177
|
+
const html = document.documentElement;
|
|
2178
|
+
let refCounter = Number.parseInt(
|
|
2179
|
+
html.getAttribute("data-unotest-ref-counter") || "0",
|
|
2180
|
+
10
|
|
2181
|
+
);
|
|
2182
|
+
if (!Number.isFinite(refCounter) || refCounter < 0) refCounter = 0;
|
|
2183
|
+
const refByElement = /* @__PURE__ */ new Map();
|
|
2184
|
+
if (!assignRefs) {
|
|
2185
|
+
for (const el of interactiveElements) {
|
|
2186
|
+
const existing = previouslyRefed.get(el);
|
|
2187
|
+
if (existing) refByElement.set(el, existing);
|
|
2188
|
+
}
|
|
2189
|
+
} else {
|
|
2190
|
+
const kept = /* @__PURE__ */ new Set();
|
|
2191
|
+
for (const el of interactiveElements) {
|
|
2192
|
+
const existing = previouslyRefed.get(el);
|
|
2193
|
+
if (existing) {
|
|
2194
|
+
refByElement.set(el, existing);
|
|
2195
|
+
el.setAttribute("data-unotest-ref-kind", "i");
|
|
2196
|
+
kept.add(el);
|
|
2197
|
+
} else {
|
|
2198
|
+
refCounter += 1;
|
|
2199
|
+
const ref = "e" + refCounter;
|
|
2200
|
+
el.setAttribute("data-unotest-ref", ref);
|
|
2201
|
+
el.setAttribute("data-unotest-ref-kind", "i");
|
|
2202
|
+
refByElement.set(el, ref);
|
|
2203
|
+
kept.add(el);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
for (const [el] of previouslyRefed) {
|
|
2207
|
+
if (kept.has(el)) continue;
|
|
2208
|
+
if (el.getAttribute("data-unotest-ref-kind") !== "i") continue;
|
|
2209
|
+
el.removeAttribute("data-unotest-ref");
|
|
2210
|
+
el.removeAttribute("data-unotest-ref-kind");
|
|
2211
|
+
}
|
|
2212
|
+
html.setAttribute("data-unotest-ref-counter", String(refCounter));
|
|
2213
|
+
}
|
|
2214
|
+
const MAX_TEXT = 80;
|
|
2215
|
+
const entries = [];
|
|
2216
|
+
for (const el of interactiveElements) {
|
|
2217
|
+
const tag = el.tagName.toLowerCase();
|
|
2218
|
+
const isClk = clickableEls.has(el);
|
|
2219
|
+
let name = accname(el);
|
|
2220
|
+
if (isClk && !name) {
|
|
2221
|
+
const t = trim(textContentVisible(el));
|
|
2222
|
+
name = t.length > MAX_TEXT ? t.slice(0, MAX_TEXT - 1) + "\u2026" : t;
|
|
2223
|
+
}
|
|
2224
|
+
if (!name) name = tooltipLabel(el) ?? "";
|
|
2225
|
+
let href;
|
|
2226
|
+
if (!name && tag === "a") {
|
|
2227
|
+
const h = shortenedHref(el.getAttribute("href"));
|
|
2228
|
+
if (h) href = h;
|
|
2229
|
+
}
|
|
2230
|
+
entries.push({
|
|
2231
|
+
el,
|
|
2232
|
+
role: computeRole(el) ?? (tag === "tr" ? "row" : tag === "li" ? "listitem" : null),
|
|
2233
|
+
name,
|
|
2234
|
+
testId: testIdOf(el),
|
|
2235
|
+
dataId: stableDataAttr(el),
|
|
2236
|
+
title: tooltipText(el, name),
|
|
2237
|
+
...isClk ? { clickable: true } : {},
|
|
2238
|
+
href,
|
|
2239
|
+
state: collectState(el),
|
|
2240
|
+
bbox: el.getBoundingClientRect(),
|
|
2241
|
+
landmark: findLandmark(el),
|
|
2242
|
+
// A row element groups as its own row-unit (findTrAncestor looks at
|
|
2243
|
+
// ANCESTORS and would return null for the <tr> itself).
|
|
2244
|
+
trAncestor: tag === "tr" ? el : findTrAncestor(el)
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
2248
|
+
let docOrderCounter = 0;
|
|
2249
|
+
function getBucket(landmark) {
|
|
2250
|
+
const key = landmark ? landmark.element : "body";
|
|
2251
|
+
let b = buckets.get(key);
|
|
2252
|
+
if (!b) {
|
|
2253
|
+
b = {
|
|
2254
|
+
kind: landmark ? landmark.kind : "body",
|
|
2255
|
+
label: landmark ? landmark.label : "",
|
|
2256
|
+
landmarkBox: landmark ? rectToBox(landmark.element.getBoundingClientRect()) : null,
|
|
2257
|
+
actionUnion: null,
|
|
2258
|
+
actions: [],
|
|
2259
|
+
firstDocOrder: docOrderCounter++
|
|
2260
|
+
};
|
|
2261
|
+
buckets.set(key, b);
|
|
2262
|
+
}
|
|
2263
|
+
return b;
|
|
2264
|
+
}
|
|
2265
|
+
__name(getBucket, "getBucket");
|
|
2266
|
+
const ROW_COLLAPSE_MIN = 5;
|
|
2267
|
+
const ROW_KEEP_HEAD = 2;
|
|
2268
|
+
const ROW_KEEP_TAIL = 1;
|
|
2269
|
+
function signatureOfRow(start, end) {
|
|
2270
|
+
const parts = [];
|
|
2271
|
+
for (let k = start; k < end; k++) {
|
|
2272
|
+
parts.push(entries[k].role ?? "");
|
|
2273
|
+
}
|
|
2274
|
+
return parts.join(",");
|
|
2275
|
+
}
|
|
2276
|
+
__name(signatureOfRow, "signatureOfRow");
|
|
2277
|
+
function collapseRowRuns() {
|
|
2278
|
+
const out = [];
|
|
2279
|
+
let i = 0;
|
|
2280
|
+
while (i < entries.length) {
|
|
2281
|
+
const tr = entries[i].trAncestor;
|
|
2282
|
+
if (tr === null) {
|
|
2283
|
+
out.push({ kind: "entry", entry: entries[i] });
|
|
2284
|
+
i++;
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
let firstEnd = i + 1;
|
|
2288
|
+
while (firstEnd < entries.length && entries[firstEnd].trAncestor === tr) {
|
|
2289
|
+
firstEnd++;
|
|
2290
|
+
}
|
|
2291
|
+
const firstSig = signatureOfRow(i, firstEnd);
|
|
2292
|
+
const runRows = [
|
|
2293
|
+
{ tr, start: i, end: firstEnd, sig: firstSig }
|
|
2294
|
+
];
|
|
2295
|
+
let scan = firstEnd;
|
|
2296
|
+
while (scan < entries.length) {
|
|
2297
|
+
const nextTr = entries[scan].trAncestor;
|
|
2298
|
+
if (nextTr === null || nextTr === tr) {
|
|
2299
|
+
break;
|
|
2300
|
+
}
|
|
2301
|
+
let nextEnd = scan + 1;
|
|
2302
|
+
while (nextEnd < entries.length && entries[nextEnd].trAncestor === nextTr) {
|
|
2303
|
+
nextEnd++;
|
|
2304
|
+
}
|
|
2305
|
+
const nextSig = signatureOfRow(scan, nextEnd);
|
|
2306
|
+
if (nextSig !== firstSig) break;
|
|
2307
|
+
runRows.push({ tr: nextTr, start: scan, end: nextEnd, sig: nextSig });
|
|
2308
|
+
scan = nextEnd;
|
|
2309
|
+
}
|
|
2310
|
+
if (runRows.length >= ROW_COLLAPSE_MIN) {
|
|
2311
|
+
for (let r = 0; r < ROW_KEEP_HEAD; r++) {
|
|
2312
|
+
const g = runRows[r];
|
|
2313
|
+
for (let k = g.start; k < g.end; k++) {
|
|
2314
|
+
out.push({ kind: "entry", entry: entries[k] });
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
const omitted = runRows.length - ROW_KEEP_HEAD - ROW_KEEP_TAIL;
|
|
2318
|
+
out.push({
|
|
2319
|
+
kind: "summary",
|
|
2320
|
+
text: `\u2026 ${omitted} similar rows \u2026`,
|
|
2321
|
+
landmark: entries[runRows[0].start].landmark
|
|
2322
|
+
});
|
|
2323
|
+
for (let r = runRows.length - ROW_KEEP_TAIL; r < runRows.length; r++) {
|
|
2324
|
+
const g = runRows[r];
|
|
2325
|
+
for (let k = g.start; k < g.end; k++) {
|
|
2326
|
+
out.push({ kind: "entry", entry: entries[k] });
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
} else {
|
|
2330
|
+
for (const g of runRows) {
|
|
2331
|
+
for (let k = g.start; k < g.end; k++) {
|
|
2332
|
+
out.push({ kind: "entry", entry: entries[k] });
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
i = scan;
|
|
2337
|
+
}
|
|
2338
|
+
return out;
|
|
2339
|
+
}
|
|
2340
|
+
__name(collapseRowRuns, "collapseRowRuns");
|
|
2341
|
+
const finalItems = collapseRowRuns();
|
|
2342
|
+
let total = 0;
|
|
2343
|
+
for (const item of finalItems) {
|
|
2344
|
+
total++;
|
|
2345
|
+
if (item.kind === "summary") {
|
|
2346
|
+
const bucket2 = getBucket(item.landmark);
|
|
2347
|
+
bucket2.actions.push({ text: item.text });
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
const e = item.entry;
|
|
2351
|
+
const action = {};
|
|
2352
|
+
if (e.role) action.role = e.role;
|
|
2353
|
+
if (e.name) action.text = e.name;
|
|
2354
|
+
if (e.testId) action.testId = e.testId;
|
|
2355
|
+
if (e.dataId) action.dataId = e.dataId;
|
|
2356
|
+
if (e.title) action.title = e.title;
|
|
2357
|
+
if (e.clickable) action.clickable = true;
|
|
2358
|
+
if (e.href) action.href = e.href;
|
|
2359
|
+
if (e.state) action.state = e.state;
|
|
2360
|
+
if (isClipped(e.bbox)) action.clipped = true;
|
|
2361
|
+
const ref = refByElement.get(e.el);
|
|
2362
|
+
if (ref) action.ref = ref;
|
|
2363
|
+
const box = rectToBox(e.bbox);
|
|
2364
|
+
action.bounds = {
|
|
2365
|
+
x: Math.round(box.x),
|
|
2366
|
+
y: Math.round(box.y),
|
|
2367
|
+
w: Math.round(box.w),
|
|
2368
|
+
h: Math.round(box.h)
|
|
2369
|
+
};
|
|
2370
|
+
const bucket = getBucket(e.landmark);
|
|
2371
|
+
bucket.actions.push(action);
|
|
2372
|
+
bucket.actionUnion = unionBox(bucket.actionUnion, box);
|
|
2373
|
+
}
|
|
2374
|
+
const regionCounts = { inView: 0, above: 0, below: 0, partial: 0 };
|
|
2375
|
+
const regions = Array.from(buckets.values()).sort((a, b) => a.firstDocOrder - b.firstDocOrder).filter((b) => b.actions.length > 0).map((b) => {
|
|
2376
|
+
const region = { kind: b.kind, actions: b.actions };
|
|
2377
|
+
if (b.label) region.label = b.label;
|
|
2378
|
+
const classifyBox = b.landmarkBox ?? b.actionUnion;
|
|
2379
|
+
const vp = classifyBox ? classifyViewport(classifyBox) : void 0;
|
|
2380
|
+
if (vp) region.viewport = vp;
|
|
2381
|
+
if (vp === "above") regionCounts.above++;
|
|
2382
|
+
else if (vp === "below") regionCounts.below++;
|
|
2383
|
+
else if (vp === "partial") regionCounts.partial++;
|
|
2384
|
+
else regionCounts.inView++;
|
|
2385
|
+
return region;
|
|
2386
|
+
});
|
|
2387
|
+
return {
|
|
2388
|
+
regions,
|
|
2389
|
+
_meta: {
|
|
2390
|
+
totalActions: total,
|
|
2391
|
+
totalRegions: regions.length,
|
|
2392
|
+
regionCounts,
|
|
2393
|
+
viewport,
|
|
2394
|
+
mode: "outline"
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
__name(outlineAlgorithm, "outlineAlgorithm");
|
|
2399
|
+
|
|
2400
|
+
// src/inspection/aria-snapshot-capture.ts
|
|
2401
|
+
import { ariaSnapshotAlgorithm as ariaSnapshotAlgorithmInjectable } from "@unotest/web/internal/page-inject";
|
|
2402
|
+
function ariaSnapshotAlgorithm() {
|
|
2403
|
+
function* deepWalk(root) {
|
|
2404
|
+
const children = root.children;
|
|
2405
|
+
for (let i = 0; i < children.length; i++) {
|
|
2406
|
+
const el = children[i];
|
|
2407
|
+
yield el;
|
|
2408
|
+
const sr = el.shadowRoot;
|
|
2409
|
+
if (sr) yield* deepWalk(sr);
|
|
2410
|
+
yield* deepWalk(el);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
__name(deepWalk, "deepWalk");
|
|
2414
|
+
function parentOrHost(node) {
|
|
2415
|
+
const parent = node.parentNode;
|
|
2416
|
+
if (!parent) return null;
|
|
2417
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
2418
|
+
return parent instanceof Element ? parent : null;
|
|
2419
|
+
}
|
|
2420
|
+
__name(parentOrHost, "parentOrHost");
|
|
2421
|
+
function rootForScope(el) {
|
|
2422
|
+
const root = el.getRootNode();
|
|
2423
|
+
return root instanceof ShadowRoot ? root : document;
|
|
2424
|
+
}
|
|
2425
|
+
__name(rootForScope, "rootForScope");
|
|
2426
|
+
function getElementByIdInScope(el, id) {
|
|
2427
|
+
return rootForScope(el).getElementById(id);
|
|
2428
|
+
}
|
|
2429
|
+
__name(getElementByIdInScope, "getElementByIdInScope");
|
|
2430
|
+
function isVisible(el) {
|
|
2431
|
+
if (!el.getClientRects || el.getClientRects().length === 0) return false;
|
|
2432
|
+
const cs = window.getComputedStyle(el);
|
|
2433
|
+
if (cs.display === "none" || cs.visibility === "hidden") return false;
|
|
2434
|
+
if (Number.parseFloat(cs.opacity || "1") < 0.01) return false;
|
|
2435
|
+
for (let p = el; p; p = parentOrHost(p)) {
|
|
2436
|
+
if (p.getAttribute && p.getAttribute("aria-hidden") === "true") return false;
|
|
2437
|
+
}
|
|
2438
|
+
const r = el.getBoundingClientRect();
|
|
2439
|
+
if (r.width === 0 && r.height === 0) return false;
|
|
2440
|
+
return true;
|
|
2441
|
+
}
|
|
2442
|
+
__name(isVisible, "isVisible");
|
|
2443
|
+
const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
2444
|
+
"button",
|
|
2445
|
+
"link",
|
|
2446
|
+
"checkbox",
|
|
2447
|
+
"radio",
|
|
2448
|
+
"menuitem",
|
|
2449
|
+
"menuitemcheckbox",
|
|
2450
|
+
"menuitemradio",
|
|
2451
|
+
"tab",
|
|
2452
|
+
"option",
|
|
2453
|
+
"switch",
|
|
2454
|
+
"combobox",
|
|
2455
|
+
"listbox",
|
|
2456
|
+
"slider",
|
|
2457
|
+
"spinbutton",
|
|
2458
|
+
"searchbox",
|
|
2459
|
+
"textbox"
|
|
2460
|
+
]);
|
|
2461
|
+
const NAMED_STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
2462
|
+
"heading",
|
|
2463
|
+
"list",
|
|
2464
|
+
"listitem",
|
|
2465
|
+
"table",
|
|
2466
|
+
"row",
|
|
2467
|
+
"cell",
|
|
2468
|
+
"columnheader",
|
|
2469
|
+
"rowheader",
|
|
2470
|
+
"img",
|
|
2471
|
+
"figure",
|
|
2472
|
+
"code",
|
|
2473
|
+
"blockquote",
|
|
2474
|
+
"separator",
|
|
2475
|
+
"group",
|
|
2476
|
+
"paragraph"
|
|
2477
|
+
]);
|
|
2478
|
+
const LANDMARK_ROLES = /* @__PURE__ */ new Set([
|
|
2479
|
+
"banner",
|
|
2480
|
+
"navigation",
|
|
2481
|
+
"main",
|
|
2482
|
+
"complementary",
|
|
2483
|
+
"contentinfo",
|
|
2484
|
+
"form",
|
|
2485
|
+
"search",
|
|
2486
|
+
"region",
|
|
2487
|
+
"dialog",
|
|
2488
|
+
"alertdialog",
|
|
2489
|
+
"article"
|
|
2490
|
+
]);
|
|
2491
|
+
function implicitRole(el) {
|
|
2492
|
+
const tag = el.tagName.toLowerCase();
|
|
2493
|
+
if (tag === "a") {
|
|
2494
|
+
return el.hasAttribute("href") ? "link" : null;
|
|
2495
|
+
}
|
|
2496
|
+
if (tag === "button") return "button";
|
|
2497
|
+
if (tag === "summary") return "button";
|
|
2498
|
+
if (tag === "textarea") return "textbox";
|
|
2499
|
+
if (tag === "select") {
|
|
2500
|
+
const s = el;
|
|
2501
|
+
return s.multiple || s.size > 1 ? "listbox" : "combobox";
|
|
2502
|
+
}
|
|
2503
|
+
if (tag === "input") {
|
|
2504
|
+
const t = (el.type || "text").toLowerCase();
|
|
2505
|
+
if (t === "button" || t === "submit" || t === "reset" || t === "image") return "button";
|
|
2506
|
+
if (t === "checkbox") return "checkbox";
|
|
2507
|
+
if (t === "radio") return "radio";
|
|
2508
|
+
if (t === "range") return "slider";
|
|
2509
|
+
if (t === "number") return "spinbutton";
|
|
2510
|
+
if (t === "search") return "searchbox";
|
|
2511
|
+
if (t === "hidden") return null;
|
|
2512
|
+
return "textbox";
|
|
2513
|
+
}
|
|
2514
|
+
if (tag === "h1" || tag === "h2" || tag === "h3" || tag === "h4" || tag === "h5" || tag === "h6") {
|
|
2515
|
+
return "heading";
|
|
2516
|
+
}
|
|
2517
|
+
if (tag === "nav") return "navigation";
|
|
2518
|
+
if (tag === "header") return "banner";
|
|
2519
|
+
if (tag === "main") return "main";
|
|
2520
|
+
if (tag === "aside") return "complementary";
|
|
2521
|
+
if (tag === "footer") return "contentinfo";
|
|
2522
|
+
if (tag === "form") return "form";
|
|
2523
|
+
if (tag === "article") return "article";
|
|
2524
|
+
if (tag === "dialog") return "dialog";
|
|
2525
|
+
if (tag === "section") {
|
|
2526
|
+
if (el.hasAttribute("aria-label") || el.hasAttribute("aria-labelledby")) {
|
|
2527
|
+
return "region";
|
|
2528
|
+
}
|
|
2529
|
+
return null;
|
|
2530
|
+
}
|
|
2531
|
+
if (tag === "ul" || tag === "ol" || tag === "menu") return "list";
|
|
2532
|
+
if (tag === "li") return "listitem";
|
|
2533
|
+
if (tag === "table") return "table";
|
|
2534
|
+
if (tag === "tr") return "row";
|
|
2535
|
+
if (tag === "td") return "cell";
|
|
2536
|
+
if (tag === "th") {
|
|
2537
|
+
const scope = (el.getAttribute("scope") || "").toLowerCase();
|
|
2538
|
+
return scope === "row" ? "rowheader" : "columnheader";
|
|
2539
|
+
}
|
|
2540
|
+
if (tag === "p") return "paragraph";
|
|
2541
|
+
if (tag === "figure") return "figure";
|
|
2542
|
+
if (tag === "figcaption") return "caption";
|
|
2543
|
+
if (tag === "img") {
|
|
2544
|
+
const alt = el.getAttribute("alt");
|
|
2545
|
+
if (alt === "") return null;
|
|
2546
|
+
return "img";
|
|
2547
|
+
}
|
|
2548
|
+
if (tag === "code") return "code";
|
|
2549
|
+
if (tag === "pre") return "code";
|
|
2550
|
+
if (tag === "blockquote") return "blockquote";
|
|
2551
|
+
if (tag === "hr") return "separator";
|
|
2552
|
+
if (tag === "details") return "group";
|
|
2553
|
+
if (tag === "fieldset") return "group";
|
|
2554
|
+
if (tag === "iframe") return "iframe";
|
|
2555
|
+
return null;
|
|
2556
|
+
}
|
|
2557
|
+
__name(implicitRole, "implicitRole");
|
|
2558
|
+
function computeRole(el) {
|
|
2559
|
+
const explicit = el.getAttribute("role");
|
|
2560
|
+
if (explicit) return explicit.split(/\s+/)[0].toLowerCase();
|
|
2561
|
+
return implicitRole(el);
|
|
2562
|
+
}
|
|
2563
|
+
__name(computeRole, "computeRole");
|
|
2564
|
+
const MAX_NAME = 80;
|
|
2565
|
+
function trim(s) {
|
|
2566
|
+
return (s ?? "").replace(/\s+/g, " ").trim();
|
|
2567
|
+
}
|
|
2568
|
+
__name(trim, "trim");
|
|
2569
|
+
function cap(s) {
|
|
2570
|
+
return s.length > MAX_NAME ? s.slice(0, MAX_NAME - 1) + "\u2026" : s;
|
|
2571
|
+
}
|
|
2572
|
+
__name(cap, "cap");
|
|
2573
|
+
function cssEscape(s) {
|
|
2574
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2575
|
+
}
|
|
2576
|
+
__name(cssEscape, "cssEscape");
|
|
2577
|
+
function textContentVisible(el) {
|
|
2578
|
+
const ht = el;
|
|
2579
|
+
if (typeof ht.innerText === "string") return ht.innerText;
|
|
2580
|
+
return el.textContent ?? "";
|
|
2581
|
+
}
|
|
2582
|
+
__name(textContentVisible, "textContentVisible");
|
|
2583
|
+
function composedAccname(el, seen) {
|
|
2584
|
+
const parts = [];
|
|
2585
|
+
for (const child of Array.from(el.childNodes)) {
|
|
2586
|
+
if (child.nodeType === 3) {
|
|
2587
|
+
const t = trim(child.nodeValue);
|
|
2588
|
+
if (t) parts.push(t);
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
2591
|
+
if (child.nodeType !== 1) continue;
|
|
2592
|
+
const ce = child;
|
|
2593
|
+
if (!isVisible(ce)) continue;
|
|
2594
|
+
if (seen.has(ce)) continue;
|
|
2595
|
+
seen.add(ce);
|
|
2596
|
+
const tag = ce.tagName.toLowerCase();
|
|
2597
|
+
if (tag === "img") {
|
|
2598
|
+
const alt = trim(ce.getAttribute("alt"));
|
|
2599
|
+
if (alt) parts.push(alt);
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
if (tag === "svg") {
|
|
2603
|
+
const sn = svgAccname(ce, seen);
|
|
2604
|
+
if (sn) parts.push(sn);
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
const sub = composedAccname(ce, seen);
|
|
2608
|
+
if (sub) parts.push(sub);
|
|
2609
|
+
}
|
|
2610
|
+
return trim(parts.join(" "));
|
|
2611
|
+
}
|
|
2612
|
+
__name(composedAccname, "composedAccname");
|
|
2613
|
+
function svgAccname(svg, seen) {
|
|
2614
|
+
const labelledby = svg.getAttribute("aria-labelledby");
|
|
2615
|
+
if (labelledby) {
|
|
2616
|
+
const parts = [];
|
|
2617
|
+
for (const id of labelledby.split(/\s+/)) {
|
|
2618
|
+
if (!id) continue;
|
|
2619
|
+
const ref = getElementByIdInScope(svg, id);
|
|
2620
|
+
if (ref && !seen.has(ref)) {
|
|
2621
|
+
const n = accname(ref, seen);
|
|
2622
|
+
if (n) parts.push(n);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
const joined = trim(parts.join(" "));
|
|
2626
|
+
if (joined) return cap(joined);
|
|
2627
|
+
}
|
|
2628
|
+
const ariaLabel = trim(svg.getAttribute("aria-label"));
|
|
2629
|
+
if (ariaLabel) return cap(ariaLabel);
|
|
2630
|
+
for (const child of Array.from(svg.children)) {
|
|
2631
|
+
if (child.tagName.toLowerCase() === "title") {
|
|
2632
|
+
const t = trim(child.textContent);
|
|
2633
|
+
if (t) return cap(t);
|
|
2634
|
+
break;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
return "";
|
|
2638
|
+
}
|
|
2639
|
+
__name(svgAccname, "svgAccname");
|
|
2640
|
+
function accname(el, visited) {
|
|
2641
|
+
const seen = visited ?? /* @__PURE__ */ new Set();
|
|
2642
|
+
if (seen.has(el)) return "";
|
|
2643
|
+
seen.add(el);
|
|
2644
|
+
const labelledby = el.getAttribute("aria-labelledby");
|
|
2645
|
+
if (labelledby) {
|
|
2646
|
+
const parts = [];
|
|
2647
|
+
for (const id of labelledby.split(/\s+/)) {
|
|
2648
|
+
if (!id) continue;
|
|
2649
|
+
const ref = getElementByIdInScope(el, id);
|
|
2650
|
+
if (ref && !seen.has(ref)) {
|
|
2651
|
+
const n = accname(ref, seen);
|
|
2652
|
+
if (n) parts.push(n);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
const joined = trim(parts.join(" "));
|
|
2656
|
+
if (joined) return cap(joined);
|
|
2657
|
+
}
|
|
2658
|
+
const ariaLabel = trim(el.getAttribute("aria-label"));
|
|
2659
|
+
if (ariaLabel) return cap(ariaLabel);
|
|
2660
|
+
const tag = el.tagName.toLowerCase();
|
|
2661
|
+
if (tag === "input" || tag === "textarea" || tag === "select") {
|
|
2662
|
+
const id = el.id;
|
|
2663
|
+
if (id) {
|
|
2664
|
+
const lbl = rootForScope(el).querySelector(
|
|
2665
|
+
`label[for="${cssEscape(id)}"]`
|
|
2666
|
+
);
|
|
2667
|
+
if (lbl) {
|
|
2668
|
+
const n = trim(textContentVisible(lbl));
|
|
2669
|
+
if (n) return cap(n);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
let p = parentOrHost(el);
|
|
2673
|
+
while (p) {
|
|
2674
|
+
if (p.tagName.toLowerCase() === "label") {
|
|
2675
|
+
const n = trim(textContentVisible(p));
|
|
2676
|
+
if (n) return cap(n);
|
|
2677
|
+
break;
|
|
2678
|
+
}
|
|
2679
|
+
p = parentOrHost(p);
|
|
2680
|
+
}
|
|
2681
|
+
if (tag === "input") {
|
|
2682
|
+
const it = el.type?.toLowerCase();
|
|
2683
|
+
if (it === "button" || it === "submit" || it === "reset") {
|
|
2684
|
+
const v = trim(el.value);
|
|
2685
|
+
if (v) return cap(v);
|
|
2686
|
+
}
|
|
2687
|
+
const ph = trim(el.placeholder);
|
|
2688
|
+
if (ph) return cap(ph);
|
|
2689
|
+
}
|
|
2690
|
+
const t2 = trim(el.getAttribute("title"));
|
|
2691
|
+
if (t2) return cap(t2);
|
|
2692
|
+
return "";
|
|
2693
|
+
}
|
|
2694
|
+
if (tag === "button" || tag === "a" || tag === "summary") {
|
|
2695
|
+
const composed = composedAccname(el, seen);
|
|
2696
|
+
if (composed) return cap(composed);
|
|
2697
|
+
const t2 = trim(el.getAttribute("title"));
|
|
2698
|
+
if (t2) return cap(t2);
|
|
2699
|
+
return "";
|
|
2700
|
+
}
|
|
2701
|
+
if (tag === "h1" || tag === "h2" || tag === "h3" || tag === "h4" || tag === "h5" || tag === "h6") {
|
|
2702
|
+
return cap(trim(textContentVisible(el)));
|
|
2703
|
+
}
|
|
2704
|
+
if (tag === "img") {
|
|
2705
|
+
return cap(trim(el.getAttribute("alt")));
|
|
2706
|
+
}
|
|
2707
|
+
if (tag === "iframe") {
|
|
2708
|
+
const t2 = trim(el.getAttribute("title"));
|
|
2709
|
+
if (t2) return cap(t2);
|
|
2710
|
+
return "";
|
|
2711
|
+
}
|
|
2712
|
+
if (tag === "figure") {
|
|
2713
|
+
for (const child of Array.from(el.children)) {
|
|
2714
|
+
if (child.tagName.toLowerCase() === "figcaption") {
|
|
2715
|
+
const t2 = trim(textContentVisible(child));
|
|
2716
|
+
if (t2) return cap(t2);
|
|
2717
|
+
break;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
return "";
|
|
2721
|
+
}
|
|
2722
|
+
if (tag === "fieldset") {
|
|
2723
|
+
for (const child of Array.from(el.children)) {
|
|
2724
|
+
if (child.tagName.toLowerCase() === "legend") {
|
|
2725
|
+
const t2 = trim(textContentVisible(child));
|
|
2726
|
+
if (t2) return cap(t2);
|
|
2727
|
+
break;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return "";
|
|
2731
|
+
}
|
|
2732
|
+
if (tag === "form" || tag === "nav" || tag === "header" || tag === "main" || tag === "aside" || tag === "footer" || tag === "section" || tag === "article" || tag === "dialog") {
|
|
2733
|
+
return "";
|
|
2734
|
+
}
|
|
2735
|
+
if (tag === "ul" || tag === "ol" || tag === "menu" || tag === "li" || tag === "table" || tag === "tr" || tag === "td" || tag === "th" || tag === "p" || tag === "code" || tag === "pre" || tag === "blockquote" || tag === "details") {
|
|
2736
|
+
return "";
|
|
2737
|
+
}
|
|
2738
|
+
const inner = trim(textContentVisible(el));
|
|
2739
|
+
if (inner) return cap(inner);
|
|
2740
|
+
const t = trim(el.getAttribute("title"));
|
|
2741
|
+
return cap(t);
|
|
2742
|
+
}
|
|
2743
|
+
__name(accname, "accname");
|
|
2744
|
+
function collectState(el, role) {
|
|
2745
|
+
const state = {};
|
|
2746
|
+
const tag = el.tagName.toLowerCase();
|
|
2747
|
+
if (el.getAttribute("aria-disabled") === "true" || el.disabled) {
|
|
2748
|
+
state.disabled = true;
|
|
2749
|
+
}
|
|
2750
|
+
const ariaChecked = el.getAttribute("aria-checked");
|
|
2751
|
+
if (ariaChecked === "true") state.checked = true;
|
|
2752
|
+
else if (ariaChecked === "mixed") state.checked = "mixed";
|
|
2753
|
+
else if (tag === "input" && (el.type === "checkbox" || el.type === "radio") && el.checked) {
|
|
2754
|
+
state.checked = true;
|
|
2755
|
+
}
|
|
2756
|
+
const ariaPressed = el.getAttribute("aria-pressed");
|
|
2757
|
+
if (ariaPressed === "true") state.pressed = true;
|
|
2758
|
+
else if (ariaPressed === "mixed") state.pressed = "mixed";
|
|
2759
|
+
if (el.getAttribute("aria-expanded") === "true") state.expanded = true;
|
|
2760
|
+
if (el.getAttribute("aria-selected") === "true") state.selected = true;
|
|
2761
|
+
else if (tag === "option" && el.selected) {
|
|
2762
|
+
state.selected = true;
|
|
2763
|
+
}
|
|
2764
|
+
if (rootForScope(el).activeElement === el) state.active = true;
|
|
2765
|
+
if (role === "heading") {
|
|
2766
|
+
if (tag === "h1") state.level = 1;
|
|
2767
|
+
else if (tag === "h2") state.level = 2;
|
|
2768
|
+
else if (tag === "h3") state.level = 3;
|
|
2769
|
+
else if (tag === "h4") state.level = 4;
|
|
2770
|
+
else if (tag === "h5") state.level = 5;
|
|
2771
|
+
else if (tag === "h6") state.level = 6;
|
|
2772
|
+
const ariaLevel = el.getAttribute("aria-level");
|
|
2773
|
+
if (ariaLevel) {
|
|
2774
|
+
const n = Number.parseInt(ariaLevel, 10);
|
|
2775
|
+
if (Number.isFinite(n) && n > 0) state.level = n;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
return Object.keys(state).length > 0 ? state : void 0;
|
|
2779
|
+
}
|
|
2780
|
+
__name(collectState, "collectState");
|
|
2781
|
+
const pageOrigin = (() => {
|
|
2782
|
+
try {
|
|
2783
|
+
return window.location.origin;
|
|
2784
|
+
} catch {
|
|
2785
|
+
return "";
|
|
2786
|
+
}
|
|
2787
|
+
})();
|
|
2788
|
+
function normaliseHref(raw) {
|
|
2789
|
+
if (!raw) return void 0;
|
|
2790
|
+
let href = raw.trim();
|
|
2791
|
+
if (!href) return void 0;
|
|
2792
|
+
if (pageOrigin && href.startsWith(pageOrigin)) {
|
|
2793
|
+
href = href.slice(pageOrigin.length) || "/";
|
|
2794
|
+
}
|
|
2795
|
+
return href;
|
|
2796
|
+
}
|
|
2797
|
+
__name(normaliseHref, "normaliseHref");
|
|
2798
|
+
const REF_REGEX = /^e\d+$/;
|
|
2799
|
+
const html = document.documentElement;
|
|
2800
|
+
let refCounter = Number.parseInt(
|
|
2801
|
+
html.getAttribute("data-unotest-ref-counter") || "0",
|
|
2802
|
+
10
|
|
2803
|
+
);
|
|
2804
|
+
if (!Number.isFinite(refCounter) || refCounter < 0) refCounter = 0;
|
|
2805
|
+
function ensureRef(el, role) {
|
|
2806
|
+
const existing = el.getAttribute("data-unotest-ref");
|
|
2807
|
+
if (existing && REF_REGEX.test(existing)) return existing;
|
|
2808
|
+
refCounter += 1;
|
|
2809
|
+
const ref = "e" + refCounter;
|
|
2810
|
+
el.setAttribute("data-unotest-ref", ref);
|
|
2811
|
+
const interactive = INTERACTIVE_ROLES.has(role);
|
|
2812
|
+
el.setAttribute("data-unotest-ref-kind", interactive ? "i" : "n");
|
|
2813
|
+
return ref;
|
|
2814
|
+
}
|
|
2815
|
+
__name(ensureRef, "ensureRef");
|
|
2816
|
+
let totalNodes = 0;
|
|
2817
|
+
let totalRefs = 0;
|
|
2818
|
+
let frameCounter = 0;
|
|
2819
|
+
function shouldInclude(role) {
|
|
2820
|
+
if (!role) return false;
|
|
2821
|
+
if (INTERACTIVE_ROLES.has(role)) return true;
|
|
2822
|
+
if (NAMED_STRUCTURAL_ROLES.has(role)) return true;
|
|
2823
|
+
if (LANDMARK_ROLES.has(role)) return true;
|
|
2824
|
+
if (role === "caption") return true;
|
|
2825
|
+
return false;
|
|
2826
|
+
}
|
|
2827
|
+
__name(shouldInclude, "shouldInclude");
|
|
2828
|
+
function makeRef(el, role, refPrefix) {
|
|
2829
|
+
const localRef = ensureRef(el, role);
|
|
2830
|
+
totalRefs += 1;
|
|
2831
|
+
return refPrefix ? `${refPrefix}${localRef}` : localRef;
|
|
2832
|
+
}
|
|
2833
|
+
__name(makeRef, "makeRef");
|
|
2834
|
+
function pushText(text, bucket, parentName) {
|
|
2835
|
+
const t = text;
|
|
2836
|
+
if (parentName && parentName === t) return;
|
|
2837
|
+
for (const existing of bucket) {
|
|
2838
|
+
if (existing.name === t) return;
|
|
2839
|
+
}
|
|
2840
|
+
bucket.push({ role: "text", name: t, children: [] });
|
|
2841
|
+
totalNodes += 1;
|
|
2842
|
+
}
|
|
2843
|
+
__name(pushText, "pushText");
|
|
2844
|
+
function descendInto(parent, bucket, refPrefix, parentName) {
|
|
2845
|
+
const buf = { value: "" };
|
|
2846
|
+
function flush() {
|
|
2847
|
+
const t = trim(buf.value);
|
|
2848
|
+
if (t) pushText(t, bucket, parentName);
|
|
2849
|
+
buf.value = "";
|
|
2850
|
+
}
|
|
2851
|
+
__name(flush, "flush");
|
|
2852
|
+
function consume(node) {
|
|
2853
|
+
if (node.nodeType === 3) {
|
|
2854
|
+
buf.value += node.nodeValue ?? "";
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
if (node.nodeType !== 1) return;
|
|
2858
|
+
const el = node;
|
|
2859
|
+
if (!isVisible(el)) return;
|
|
2860
|
+
const tag = el.tagName.toLowerCase();
|
|
2861
|
+
if (tag === "script" || tag === "style" || tag === "noscript") return;
|
|
2862
|
+
if (tag === "iframe") {
|
|
2863
|
+
flush();
|
|
2864
|
+
visit(el, bucket, refPrefix, parentName);
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
const role = computeRole(el);
|
|
2868
|
+
if (shouldInclude(role)) {
|
|
2869
|
+
flush();
|
|
2870
|
+
visit(el, bucket, refPrefix, parentName);
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
const sr2 = el.shadowRoot;
|
|
2874
|
+
if (sr2) {
|
|
2875
|
+
for (const c of Array.from(sr2.childNodes)) consume(c);
|
|
2876
|
+
}
|
|
2877
|
+
for (const c of Array.from(el.childNodes)) consume(c);
|
|
2878
|
+
}
|
|
2879
|
+
__name(consume, "consume");
|
|
2880
|
+
const sr = parent.shadowRoot;
|
|
2881
|
+
if (sr) {
|
|
2882
|
+
for (const c of Array.from(sr.childNodes)) consume(c);
|
|
2883
|
+
}
|
|
2884
|
+
for (const c of Array.from(parent.childNodes)) consume(c);
|
|
2885
|
+
flush();
|
|
2886
|
+
}
|
|
2887
|
+
__name(descendInto, "descendInto");
|
|
2888
|
+
function visit(el, parentBucket, refPrefix, _parentName) {
|
|
2889
|
+
const tag = el.tagName.toLowerCase();
|
|
2890
|
+
if (tag === "iframe") {
|
|
2891
|
+
const name2 = accname(el);
|
|
2892
|
+
const ref = makeRef(el, "iframe", refPrefix);
|
|
2893
|
+
const node2 = { role: "iframe", name: name2, ref, children: [] };
|
|
2894
|
+
let inner = null;
|
|
2895
|
+
try {
|
|
2896
|
+
inner = el.contentDocument;
|
|
2897
|
+
} catch {
|
|
2898
|
+
inner = null;
|
|
2899
|
+
}
|
|
2900
|
+
if (inner && inner.body) {
|
|
2901
|
+
frameCounter += 1;
|
|
2902
|
+
const innerPrefix = `f${frameCounter}`;
|
|
2903
|
+
descendInto(inner.body, node2.children, innerPrefix, name2);
|
|
2904
|
+
}
|
|
2905
|
+
totalNodes += 1;
|
|
2906
|
+
parentBucket.push(node2);
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
const role = computeRole(el);
|
|
2910
|
+
const name = accname(el);
|
|
2911
|
+
const node = { role, name, children: [] };
|
|
2912
|
+
node.ref = makeRef(el, role, refPrefix);
|
|
2913
|
+
if (role === "link") {
|
|
2914
|
+
const url = normaliseHref(
|
|
2915
|
+
el.getAttribute("href")
|
|
2916
|
+
);
|
|
2917
|
+
if (url) node.url = url;
|
|
2918
|
+
}
|
|
2919
|
+
const state = collectState(el, role);
|
|
2920
|
+
if (state) node.state = state;
|
|
2921
|
+
totalNodes += 1;
|
|
2922
|
+
parentBucket.push(node);
|
|
2923
|
+
if (role === "paragraph" || role === "code") {
|
|
2924
|
+
if (!node.name) {
|
|
2925
|
+
const t = trim(textContentVisible(el));
|
|
2926
|
+
if (t) node.name = t;
|
|
2927
|
+
}
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
if (role === "heading") return;
|
|
2931
|
+
if (role === "button" || role === "tab" || role === "menuitem" || role === "option") {
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
descendInto(el, node.children, refPrefix, name);
|
|
2935
|
+
}
|
|
2936
|
+
__name(visit, "visit");
|
|
2937
|
+
const ROW_COLLAPSE_MIN = 5;
|
|
2938
|
+
const ROW_KEEP_HEAD = 2;
|
|
2939
|
+
const ROW_KEEP_TAIL = 1;
|
|
2940
|
+
function signatureOf(node) {
|
|
2941
|
+
const roles = node.children.map((c) => c.role).join(",");
|
|
2942
|
+
return `${node.role}:[${roles}]`;
|
|
2943
|
+
}
|
|
2944
|
+
__name(signatureOf, "signatureOf");
|
|
2945
|
+
function collapseRepetitiveRows(node) {
|
|
2946
|
+
for (const c of node.children) collapseRepetitiveRows(c);
|
|
2947
|
+
if (node.children.length < ROW_COLLAPSE_MIN) return;
|
|
2948
|
+
const out = [];
|
|
2949
|
+
let i = 0;
|
|
2950
|
+
while (i < node.children.length) {
|
|
2951
|
+
const sig = signatureOf(node.children[i]);
|
|
2952
|
+
const isRowRun = node.children[i].role === "row";
|
|
2953
|
+
let j = i + 1;
|
|
2954
|
+
if (isRowRun) {
|
|
2955
|
+
while (j < node.children.length && node.children[j].role === "row" && signatureOf(node.children[j]) === sig) {
|
|
2956
|
+
j++;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
const runLen = j - i;
|
|
2960
|
+
if (isRowRun && runLen >= ROW_COLLAPSE_MIN) {
|
|
2961
|
+
for (let k = 0; k < ROW_KEEP_HEAD; k++) out.push(node.children[i + k]);
|
|
2962
|
+
const omitted = runLen - ROW_KEEP_HEAD - ROW_KEEP_TAIL;
|
|
2963
|
+
out.push({
|
|
2964
|
+
role: "text",
|
|
2965
|
+
name: `\u2026 ${omitted} similar rows \u2026`,
|
|
2966
|
+
children: []
|
|
2967
|
+
});
|
|
2968
|
+
for (let k = ROW_KEEP_TAIL; k > 0; k--) {
|
|
2969
|
+
out.push(node.children[j - k]);
|
|
2970
|
+
}
|
|
2971
|
+
} else {
|
|
2972
|
+
for (let k = i; k < j; k++) out.push(node.children[k]);
|
|
2973
|
+
if (!isRowRun) j = i + 1;
|
|
2974
|
+
}
|
|
2975
|
+
i = j;
|
|
2976
|
+
}
|
|
2977
|
+
node.children = out;
|
|
2978
|
+
}
|
|
2979
|
+
__name(collapseRepetitiveRows, "collapseRepetitiveRows");
|
|
2980
|
+
function recountTree(roots) {
|
|
2981
|
+
let nodes2 = 0;
|
|
2982
|
+
let refs = 0;
|
|
2983
|
+
function walk(n) {
|
|
2984
|
+
nodes2 += 1;
|
|
2985
|
+
if (n.ref) refs += 1;
|
|
2986
|
+
for (const c of n.children) walk(c);
|
|
2987
|
+
}
|
|
2988
|
+
__name(walk, "walk");
|
|
2989
|
+
for (const r of roots) walk(r);
|
|
2990
|
+
return { nodes: nodes2, refs };
|
|
2991
|
+
}
|
|
2992
|
+
__name(recountTree, "recountTree");
|
|
2993
|
+
const rootBucket = [];
|
|
2994
|
+
const body = document.body;
|
|
2995
|
+
if (body) {
|
|
2996
|
+
descendInto(body, rootBucket, "", "");
|
|
2997
|
+
}
|
|
2998
|
+
html.setAttribute("data-unotest-ref-counter", String(refCounter));
|
|
2999
|
+
for (const r of rootBucket) collapseRepetitiveRows(r);
|
|
3000
|
+
const recounted = recountTree(rootBucket);
|
|
3001
|
+
totalNodes = recounted.nodes;
|
|
3002
|
+
totalRefs = recounted.refs;
|
|
3003
|
+
const nodes = rootBucket;
|
|
3004
|
+
return {
|
|
3005
|
+
nodes,
|
|
3006
|
+
_meta: {
|
|
3007
|
+
totalNodes,
|
|
3008
|
+
totalRefs,
|
|
3009
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
3010
|
+
mode: "aria"
|
|
3011
|
+
}
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
__name(ariaSnapshotAlgorithm, "ariaSnapshotAlgorithm");
|
|
3015
|
+
|
|
3016
|
+
// src/inspection/full-html-capture.ts
|
|
3017
|
+
import { serializeRuntimeDom as serializeRuntimeDomInjectable } from "@unotest/web/internal/page-inject";
|
|
3018
|
+
function serializeRuntimeDom() {
|
|
3019
|
+
const VOID = /* @__PURE__ */ new Set([
|
|
3020
|
+
"area",
|
|
3021
|
+
"base",
|
|
3022
|
+
"br",
|
|
3023
|
+
"col",
|
|
3024
|
+
"embed",
|
|
3025
|
+
"hr",
|
|
3026
|
+
"img",
|
|
3027
|
+
"input",
|
|
3028
|
+
"link",
|
|
3029
|
+
"meta",
|
|
3030
|
+
"param",
|
|
3031
|
+
"source",
|
|
3032
|
+
"track",
|
|
3033
|
+
"wbr"
|
|
3034
|
+
]);
|
|
3035
|
+
function escText(s) {
|
|
3036
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3037
|
+
}
|
|
3038
|
+
__name(escText, "escText");
|
|
3039
|
+
function escAttr(s) {
|
|
3040
|
+
return s.replace(/&/g, "&").replace(/"/g, """);
|
|
3041
|
+
}
|
|
3042
|
+
__name(escAttr, "escAttr");
|
|
3043
|
+
function isLiveControlledAttr(tag, name) {
|
|
3044
|
+
return tag === "input" && (name === "value" || name === "checked");
|
|
3045
|
+
}
|
|
3046
|
+
__name(isLiveControlledAttr, "isLiveControlledAttr");
|
|
3047
|
+
function runtimeAttrs(el) {
|
|
3048
|
+
const tag = el.tagName.toLowerCase();
|
|
3049
|
+
if (tag !== "input") return "";
|
|
3050
|
+
const inp = el;
|
|
3051
|
+
const type = (inp.type || "text").toLowerCase();
|
|
3052
|
+
if (type === "checkbox" || type === "radio") {
|
|
3053
|
+
return inp.checked ? " checked" : "";
|
|
3054
|
+
}
|
|
3055
|
+
return ` value="${escAttr(inp.value)}"`;
|
|
3056
|
+
}
|
|
3057
|
+
__name(runtimeAttrs, "runtimeAttrs");
|
|
3058
|
+
function serialize(node) {
|
|
3059
|
+
if (node.nodeType === 3) return escText(node.nodeValue ?? "");
|
|
3060
|
+
if (node.nodeType === 8) return `<!--${node.nodeValue ?? ""}-->`;
|
|
3061
|
+
if (node.nodeType !== 1) return "";
|
|
3062
|
+
const el = node;
|
|
3063
|
+
const tag = el.tagName.toLowerCase();
|
|
3064
|
+
if (tag === "script" || tag === "style") {
|
|
3065
|
+
let a = "";
|
|
3066
|
+
for (const attr of Array.from(el.attributes)) a += ` ${attr.name}="${escAttr(attr.value)}"`;
|
|
3067
|
+
return `<${tag}${a}>${el.textContent ?? ""}</${tag}>`;
|
|
3068
|
+
}
|
|
3069
|
+
let attrs = "";
|
|
3070
|
+
for (const attr of Array.from(el.attributes)) {
|
|
3071
|
+
if (isLiveControlledAttr(tag, attr.name)) continue;
|
|
3072
|
+
attrs += ` ${attr.name}="${escAttr(attr.value)}"`;
|
|
3073
|
+
}
|
|
3074
|
+
attrs += runtimeAttrs(el);
|
|
3075
|
+
let html = `<${tag}${attrs}>`;
|
|
3076
|
+
if (VOID.has(tag)) return html;
|
|
3077
|
+
const sr = el.shadowRoot;
|
|
3078
|
+
if (sr) {
|
|
3079
|
+
html += `<template shadowrootmode="open">`;
|
|
3080
|
+
for (const c of Array.from(sr.childNodes)) html += serialize(c);
|
|
3081
|
+
html += `</template>`;
|
|
3082
|
+
}
|
|
3083
|
+
if (tag === "textarea") {
|
|
3084
|
+
html += escText(el.value);
|
|
3085
|
+
} else {
|
|
3086
|
+
for (const c of Array.from(el.childNodes)) html += serialize(c);
|
|
3087
|
+
}
|
|
3088
|
+
html += `</${tag}>`;
|
|
3089
|
+
return html;
|
|
3090
|
+
}
|
|
3091
|
+
__name(serialize, "serialize");
|
|
3092
|
+
return "<!DOCTYPE html>\n" + serialize(document.documentElement);
|
|
3093
|
+
}
|
|
3094
|
+
__name(serializeRuntimeDom, "serializeRuntimeDom");
|
|
3095
|
+
|
|
3096
|
+
// src/inspection/find-element.ts
|
|
3097
|
+
import { findElementAlgorithm as findElementAlgorithmInjectable } from "@unotest/web/internal/page-inject";
|
|
3098
|
+
function findElementAlgorithm(args) {
|
|
3099
|
+
function* deepWalk(root) {
|
|
3100
|
+
const children = root.children;
|
|
3101
|
+
for (let i = 0; i < children.length; i++) {
|
|
3102
|
+
const el = children[i];
|
|
3103
|
+
yield el;
|
|
3104
|
+
const sr = el.shadowRoot;
|
|
3105
|
+
if (sr) yield* deepWalk(sr);
|
|
3106
|
+
yield* deepWalk(el);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
__name(deepWalk, "deepWalk");
|
|
3110
|
+
function parentOrHost(node) {
|
|
3111
|
+
const parent = node.parentNode;
|
|
3112
|
+
if (!parent) return null;
|
|
3113
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
3114
|
+
return parent instanceof Element ? parent : null;
|
|
3115
|
+
}
|
|
3116
|
+
__name(parentOrHost, "parentOrHost");
|
|
3117
|
+
function rootForScope(el) {
|
|
3118
|
+
const root = el.getRootNode();
|
|
3119
|
+
return root instanceof ShadowRoot ? root : document;
|
|
3120
|
+
}
|
|
3121
|
+
__name(rootForScope, "rootForScope");
|
|
3122
|
+
function getElementByIdInScope(el, id) {
|
|
3123
|
+
return rootForScope(el).getElementById(id);
|
|
3124
|
+
}
|
|
3125
|
+
__name(getElementByIdInScope, "getElementByIdInScope");
|
|
3126
|
+
function isVisible(el) {
|
|
3127
|
+
if (!el.getClientRects || el.getClientRects().length === 0) return false;
|
|
3128
|
+
const cs = window.getComputedStyle(el);
|
|
3129
|
+
if (cs.display === "none" || cs.visibility === "hidden") return false;
|
|
3130
|
+
if (Number.parseFloat(cs.opacity || "1") < 0.01) return false;
|
|
3131
|
+
for (let p = el; p; p = parentOrHost(p)) {
|
|
3132
|
+
if (p.getAttribute && p.getAttribute("aria-hidden") === "true") return false;
|
|
3133
|
+
}
|
|
3134
|
+
const r = el.getBoundingClientRect();
|
|
3135
|
+
if (r.width === 0 && r.height === 0) return false;
|
|
3136
|
+
return true;
|
|
3137
|
+
}
|
|
3138
|
+
__name(isVisible, "isVisible");
|
|
3139
|
+
function implicitRole(el) {
|
|
3140
|
+
const tag = el.tagName.toLowerCase();
|
|
3141
|
+
if (tag === "a") return el.hasAttribute("href") ? "link" : null;
|
|
3142
|
+
if (tag === "button") return "button";
|
|
3143
|
+
if (tag === "summary") return "button";
|
|
3144
|
+
if (tag === "textarea") return "textbox";
|
|
3145
|
+
if (tag === "select") {
|
|
3146
|
+
const s = el;
|
|
3147
|
+
return s.multiple || s.size > 1 ? "listbox" : "combobox";
|
|
3148
|
+
}
|
|
3149
|
+
if (tag === "input") {
|
|
3150
|
+
const t = (el.type || "text").toLowerCase();
|
|
3151
|
+
if (t === "button" || t === "submit" || t === "reset" || t === "image") return "button";
|
|
3152
|
+
if (t === "checkbox") return "checkbox";
|
|
3153
|
+
if (t === "radio") return "radio";
|
|
3154
|
+
if (t === "range") return "slider";
|
|
3155
|
+
if (t === "number") return "spinbutton";
|
|
3156
|
+
if (t === "search") return "searchbox";
|
|
3157
|
+
if (t === "hidden") return null;
|
|
3158
|
+
return "textbox";
|
|
3159
|
+
}
|
|
3160
|
+
if (/^h[1-6]$/.test(tag)) return "heading";
|
|
3161
|
+
if (tag === "nav") return "navigation";
|
|
3162
|
+
if (tag === "header") return "banner";
|
|
3163
|
+
if (tag === "main") return "main";
|
|
3164
|
+
if (tag === "aside") return "complementary";
|
|
3165
|
+
if (tag === "footer") return "contentinfo";
|
|
3166
|
+
if (tag === "form") return "form";
|
|
3167
|
+
if (tag === "article") return "article";
|
|
3168
|
+
if (tag === "dialog") return "dialog";
|
|
3169
|
+
if (tag === "section") {
|
|
3170
|
+
return el.hasAttribute("aria-label") || el.hasAttribute("aria-labelledby") ? "region" : null;
|
|
3171
|
+
}
|
|
3172
|
+
if (tag === "ul" || tag === "ol" || tag === "menu") return "list";
|
|
3173
|
+
if (tag === "li") return "listitem";
|
|
3174
|
+
if (tag === "table") return "table";
|
|
3175
|
+
if (tag === "tr") return "row";
|
|
3176
|
+
if (tag === "td") return "cell";
|
|
3177
|
+
if (tag === "th") {
|
|
3178
|
+
const scope = (el.getAttribute("scope") || "").toLowerCase();
|
|
3179
|
+
return scope === "row" ? "rowheader" : "columnheader";
|
|
3180
|
+
}
|
|
3181
|
+
if (tag === "p") return "paragraph";
|
|
3182
|
+
if (tag === "figure") return "figure";
|
|
3183
|
+
if (tag === "img") {
|
|
3184
|
+
const alt = el.getAttribute("alt");
|
|
3185
|
+
if (alt === "") return null;
|
|
3186
|
+
return "img";
|
|
3187
|
+
}
|
|
3188
|
+
if (tag === "details" || tag === "fieldset") return "group";
|
|
3189
|
+
if (tag === "iframe") return "iframe";
|
|
3190
|
+
return null;
|
|
3191
|
+
}
|
|
3192
|
+
__name(implicitRole, "implicitRole");
|
|
3193
|
+
function computeRole(el) {
|
|
3194
|
+
const explicit = el.getAttribute("role");
|
|
3195
|
+
if (explicit) return explicit.split(/\s+/)[0].toLowerCase();
|
|
3196
|
+
return implicitRole(el);
|
|
3197
|
+
}
|
|
3198
|
+
__name(computeRole, "computeRole");
|
|
3199
|
+
function trim(s) {
|
|
3200
|
+
return (s ?? "").replace(/\s+/g, " ").trim();
|
|
3201
|
+
}
|
|
3202
|
+
__name(trim, "trim");
|
|
3203
|
+
function textContentVisible(el) {
|
|
3204
|
+
const ht = el;
|
|
3205
|
+
if (typeof ht.innerText === "string") return ht.innerText;
|
|
3206
|
+
return el.textContent ?? "";
|
|
3207
|
+
}
|
|
3208
|
+
__name(textContentVisible, "textContentVisible");
|
|
3209
|
+
const MAX_NAME = 200;
|
|
3210
|
+
function cap(s) {
|
|
3211
|
+
return s.length > MAX_NAME ? s.slice(0, MAX_NAME - 1) + "\u2026" : s;
|
|
3212
|
+
}
|
|
3213
|
+
__name(cap, "cap");
|
|
3214
|
+
function accname(el, visited) {
|
|
3215
|
+
const seen = visited ?? /* @__PURE__ */ new Set();
|
|
3216
|
+
if (seen.has(el)) return "";
|
|
3217
|
+
seen.add(el);
|
|
3218
|
+
const labelledby = el.getAttribute("aria-labelledby");
|
|
3219
|
+
if (labelledby) {
|
|
3220
|
+
const parts = [];
|
|
3221
|
+
for (const id of labelledby.split(/\s+/)) {
|
|
3222
|
+
if (!id) continue;
|
|
3223
|
+
const ref = getElementByIdInScope(el, id);
|
|
3224
|
+
if (ref && !seen.has(ref)) {
|
|
3225
|
+
const n = accname(ref, seen);
|
|
3226
|
+
if (n) parts.push(n);
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
const joined = trim(parts.join(" "));
|
|
3230
|
+
if (joined) return cap(joined);
|
|
3231
|
+
}
|
|
3232
|
+
const ariaLabel = trim(el.getAttribute("aria-label"));
|
|
3233
|
+
if (ariaLabel) return cap(ariaLabel);
|
|
3234
|
+
const tag = el.tagName.toLowerCase();
|
|
3235
|
+
if (tag === "input" || tag === "textarea") {
|
|
3236
|
+
const ph = trim(el.placeholder);
|
|
3237
|
+
if (ph) return cap(ph);
|
|
3238
|
+
const t = trim(el.getAttribute("title"));
|
|
3239
|
+
if (t) return cap(t);
|
|
3240
|
+
return "";
|
|
3241
|
+
}
|
|
3242
|
+
if (tag === "img") return cap(trim(el.getAttribute("alt")));
|
|
3243
|
+
if (tag === "form" || tag === "nav" || tag === "header" || tag === "main" || tag === "aside" || tag === "footer" || tag === "section" || tag === "article" || tag === "dialog" || tag === "ul" || tag === "ol" || tag === "menu" || tag === "li" || tag === "table" || tag === "tr" || tag === "td" || tag === "th" || tag === "p" || tag === "code" || tag === "pre" || tag === "blockquote" || tag === "fieldset" || tag === "details") {
|
|
3244
|
+
return "";
|
|
3245
|
+
}
|
|
3246
|
+
return cap(trim(textContentVisible(el)));
|
|
3247
|
+
}
|
|
3248
|
+
__name(accname, "accname");
|
|
3249
|
+
const REF_REGEX = /^e\d+$/;
|
|
3250
|
+
const html = document.documentElement;
|
|
3251
|
+
let refCounter = Number.parseInt(
|
|
3252
|
+
html.getAttribute("data-unotest-ref-counter") || "0",
|
|
3253
|
+
10
|
|
3254
|
+
);
|
|
3255
|
+
if (!Number.isFinite(refCounter) || refCounter < 0) refCounter = 0;
|
|
3256
|
+
function ensureRef(el) {
|
|
3257
|
+
const existing = el.getAttribute("data-unotest-ref");
|
|
3258
|
+
if (existing && REF_REGEX.test(existing)) return existing;
|
|
3259
|
+
refCounter += 1;
|
|
3260
|
+
const ref = "e" + refCounter;
|
|
3261
|
+
el.setAttribute("data-unotest-ref", ref);
|
|
3262
|
+
el.setAttribute("data-unotest-ref-kind", "n");
|
|
3263
|
+
return ref;
|
|
3264
|
+
}
|
|
3265
|
+
__name(ensureRef, "ensureRef");
|
|
3266
|
+
let scopeRoot = document;
|
|
3267
|
+
if (args.near) {
|
|
3268
|
+
const scoped = document.querySelector(
|
|
3269
|
+
`[data-unotest-ref="${args.near}"]`
|
|
3270
|
+
);
|
|
3271
|
+
if (scoped) scopeRoot = scoped;
|
|
3272
|
+
}
|
|
3273
|
+
const wantName = args.name ? args.name.toLowerCase() : null;
|
|
3274
|
+
const hits = [];
|
|
3275
|
+
function visitOne(el) {
|
|
3276
|
+
if (!isVisible(el)) return;
|
|
3277
|
+
const role = computeRole(el);
|
|
3278
|
+
if (role === args.role) {
|
|
3279
|
+
if (wantName === null) {
|
|
3280
|
+
hits.push(el);
|
|
3281
|
+
} else if (accname(el).toLowerCase().includes(wantName)) {
|
|
3282
|
+
hits.push(el);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
__name(visitOne, "visitOne");
|
|
3287
|
+
if (scopeRoot !== document) {
|
|
3288
|
+
const sr = scopeRoot.shadowRoot;
|
|
3289
|
+
if (sr) {
|
|
3290
|
+
for (const el of deepWalk(sr)) visitOne(el);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
for (const el of deepWalk(scopeRoot)) visitOne(el);
|
|
3294
|
+
const totalCount = hits.length;
|
|
3295
|
+
let selected = hits;
|
|
3296
|
+
if (args.nth !== void 0) {
|
|
3297
|
+
const i = args.nth;
|
|
3298
|
+
selected = i >= 0 && i < hits.length ? [hits[i]] : [];
|
|
3299
|
+
} else {
|
|
3300
|
+
selected = hits.slice(0, args.limit);
|
|
3301
|
+
}
|
|
3302
|
+
function namedAncestors(el) {
|
|
3303
|
+
const chain = [];
|
|
3304
|
+
let p = parentOrHost(el);
|
|
3305
|
+
while (p) {
|
|
3306
|
+
const role = computeRole(p);
|
|
3307
|
+
if (role) {
|
|
3308
|
+
const name = accname(p);
|
|
3309
|
+
if (role !== "generic" || name) {
|
|
3310
|
+
chain.push({ role, name, ref: ensureRef(p) });
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
p = parentOrHost(p);
|
|
3314
|
+
}
|
|
3315
|
+
return chain.reverse().slice(-4);
|
|
3316
|
+
}
|
|
3317
|
+
__name(namedAncestors, "namedAncestors");
|
|
3318
|
+
const matches = selected.map((el, idx) => ({
|
|
3319
|
+
ref: ensureRef(el),
|
|
3320
|
+
role: args.role,
|
|
3321
|
+
name: accname(el),
|
|
3322
|
+
index: args.nth !== void 0 ? args.nth : idx,
|
|
3323
|
+
ancestors: namedAncestors(el)
|
|
3324
|
+
}));
|
|
3325
|
+
html.setAttribute("data-unotest-ref-counter", String(refCounter));
|
|
3326
|
+
const query = { role: args.role };
|
|
3327
|
+
if (args.name !== void 0) query.name = args.name;
|
|
3328
|
+
if (args.near !== void 0) query.near = args.near;
|
|
3329
|
+
if (args.nth !== void 0) query.nth = args.nth;
|
|
3330
|
+
return { matches, totalCount, query };
|
|
3331
|
+
}
|
|
3332
|
+
__name(findElementAlgorithm, "findElementAlgorithm");
|
|
3333
|
+
|
|
3334
|
+
// src/inspection/page-dom.ts
|
|
3335
|
+
function pasteValue(el, pasted) {
|
|
3336
|
+
const input = el;
|
|
3337
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
3338
|
+
input.constructor.prototype,
|
|
3339
|
+
"value"
|
|
3340
|
+
)?.set;
|
|
3341
|
+
if (setter && typeof input.value === "string") {
|
|
3342
|
+
setter.call(input, pasted);
|
|
3343
|
+
} else {
|
|
3344
|
+
input.value = pasted;
|
|
3345
|
+
}
|
|
3346
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3347
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3348
|
+
}
|
|
3349
|
+
__name(pasteValue, "pasteValue");
|
|
3350
|
+
function readOuterHtml(el) {
|
|
3351
|
+
globalThis.__name ??= (fn) => fn;
|
|
3352
|
+
return el.outerHTML;
|
|
3353
|
+
}
|
|
3354
|
+
__name(readOuterHtml, "readOuterHtml");
|
|
3355
|
+
function inspectElementRaw(el) {
|
|
3356
|
+
globalThis.__name ??= (fn) => fn;
|
|
3357
|
+
const e = el;
|
|
3358
|
+
const ds = e.dataset;
|
|
3359
|
+
const testId = ds.testid ?? ds.qa ?? e.getAttribute("data-testid") ?? e.getAttribute("data-qa");
|
|
3360
|
+
const ariaLabel = e.getAttribute("aria-label") ?? void 0;
|
|
3361
|
+
const explicitRole = e.getAttribute("role") ?? void 0;
|
|
3362
|
+
const tag = e.tagName.toLowerCase();
|
|
3363
|
+
const inferredRole = inferRole(tag, e);
|
|
3364
|
+
const role = explicitRole ?? inferredRole;
|
|
3365
|
+
let name = ariaLabel;
|
|
3366
|
+
if (!name) {
|
|
3367
|
+
const labelledBy = e.getAttribute("aria-labelledby");
|
|
3368
|
+
if (labelledBy) {
|
|
3369
|
+
const ids = labelledBy.split(/\s+/);
|
|
3370
|
+
const parts = [];
|
|
3371
|
+
for (const id of ids) {
|
|
3372
|
+
const n = e.ownerDocument?.getElementById(id);
|
|
3373
|
+
if (n) parts.push((n.textContent ?? "").trim());
|
|
3374
|
+
}
|
|
3375
|
+
name = parts.filter(Boolean).join(" ") || void 0;
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
const STRUCTURAL_TAGS = /* @__PURE__ */ new Set([
|
|
3379
|
+
"form",
|
|
3380
|
+
"nav",
|
|
3381
|
+
"header",
|
|
3382
|
+
"main",
|
|
3383
|
+
"aside",
|
|
3384
|
+
"footer",
|
|
3385
|
+
"section",
|
|
3386
|
+
"article",
|
|
3387
|
+
"dialog",
|
|
3388
|
+
"ul",
|
|
3389
|
+
"ol",
|
|
3390
|
+
"menu",
|
|
3391
|
+
"li",
|
|
3392
|
+
"table",
|
|
3393
|
+
"tr",
|
|
3394
|
+
"td",
|
|
3395
|
+
"th",
|
|
3396
|
+
"p",
|
|
3397
|
+
"code",
|
|
3398
|
+
"pre",
|
|
3399
|
+
"blockquote",
|
|
3400
|
+
"fieldset",
|
|
3401
|
+
"details",
|
|
3402
|
+
"thead",
|
|
3403
|
+
"tbody",
|
|
3404
|
+
"tfoot",
|
|
3405
|
+
"colgroup",
|
|
3406
|
+
"col"
|
|
3407
|
+
]);
|
|
3408
|
+
const rawText = STRUCTURAL_TAGS.has(tag) ? "" : (e.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
3409
|
+
const text = rawText || void 0;
|
|
3410
|
+
if (!name && text) name = text;
|
|
3411
|
+
const href = tag === "a" ? e.getAttribute("href") ?? void 0 : void 0;
|
|
3412
|
+
const placeholder = e.placeholder?.trim() || void 0;
|
|
3413
|
+
const alt = e.getAttribute("alt")?.trim() || void 0;
|
|
3414
|
+
const titleAttr = e.getAttribute("title")?.trim() || void 0;
|
|
3415
|
+
const rawId = e.id;
|
|
3416
|
+
const idLooksStable = /* @__PURE__ */ __name((id) => {
|
|
3417
|
+
if (!id || id.length > 40) return false;
|
|
3418
|
+
if (id.startsWith(":") || id.startsWith("__")) return false;
|
|
3419
|
+
const digits = id.replace(/\D/g, "").length;
|
|
3420
|
+
return !(id.length > 8 && digits * 2 > id.length);
|
|
3421
|
+
}, "idLooksStable");
|
|
3422
|
+
const elementId = rawId && idLooksStable(rawId) ? rawId : void 0;
|
|
3423
|
+
const nameAttr = e.getAttribute("name")?.trim() || void 0;
|
|
3424
|
+
function dataIdOf(el2) {
|
|
3425
|
+
const skip = /^data-(unotest|v-|react|ng-|svelte|qa|testid|sort|shadow|tooltip|toggle|target|bs-|original-title)/;
|
|
3426
|
+
const idLike = /(^|[-_])(id|guid|uid|key|code|ref|num|no)$/;
|
|
3427
|
+
for (const a of Array.from(el2.attributes)) {
|
|
3428
|
+
if (!a.name.startsWith("data-") || skip.test(a.name) || !idLike.test(a.name)) continue;
|
|
3429
|
+
const v = (a.value ?? "").trim();
|
|
3430
|
+
if (!v || v.length > 64) continue;
|
|
3431
|
+
if (/^\d+$/.test(v) || idLooksStable(v)) return { name: a.name, value: v };
|
|
3432
|
+
}
|
|
3433
|
+
return void 0;
|
|
3434
|
+
}
|
|
3435
|
+
__name(dataIdOf, "dataIdOf");
|
|
3436
|
+
const dataAttr = dataIdOf(e);
|
|
3437
|
+
const tooltipAttr = (() => {
|
|
3438
|
+
for (const a of Array.from(e.attributes)) {
|
|
3439
|
+
if (!a.name.startsWith("data-") || a.name.startsWith("data-unotest")) continue;
|
|
3440
|
+
if (!/(^|-)(title|tooltip|label)$/.test(a.name)) continue;
|
|
3441
|
+
const v = (a.value ?? "").trim();
|
|
3442
|
+
if (v && v.length <= 120) return { name: a.name, value: v };
|
|
3443
|
+
}
|
|
3444
|
+
return void 0;
|
|
3445
|
+
})();
|
|
3446
|
+
function stableClassOf(el2) {
|
|
3447
|
+
const cls = (el2.getAttribute("class") ?? "").trim();
|
|
3448
|
+
if (!cls) return void 0;
|
|
3449
|
+
for (const c of cls.split(/\s+/)) {
|
|
3450
|
+
if (c.includes(":") || c.length < 3 || c.length > 30) continue;
|
|
3451
|
+
const digits = c.replace(/\D/g, "").length;
|
|
3452
|
+
if (digits * 2 > c.length) continue;
|
|
3453
|
+
if (/^(active|open|show|hidden|selected|disabled|hover|focus)$/.test(c)) continue;
|
|
3454
|
+
return c;
|
|
3455
|
+
}
|
|
3456
|
+
return void 0;
|
|
3457
|
+
}
|
|
3458
|
+
__name(stableClassOf, "stableClassOf");
|
|
3459
|
+
function parentOrHost(node) {
|
|
3460
|
+
const p = node.parentNode;
|
|
3461
|
+
if (!p) return null;
|
|
3462
|
+
if (p instanceof ShadowRoot) return p.host;
|
|
3463
|
+
return p instanceof Element ? p : null;
|
|
3464
|
+
}
|
|
3465
|
+
__name(parentOrHost, "parentOrHost");
|
|
3466
|
+
const ancestors = [];
|
|
3467
|
+
for (let p = parentOrHost(e); p && ancestors.length < 6; p = parentOrHost(p)) {
|
|
3468
|
+
const tid = p.dataset?.testid ?? p.getAttribute("data-qa") ?? void 0;
|
|
3469
|
+
const did = dataIdOf(p);
|
|
3470
|
+
const pid = p.id && idLooksStable(p.id) ? p.id : void 0;
|
|
3471
|
+
const cls = stableClassOf(p);
|
|
3472
|
+
if (tid || did || pid || cls) {
|
|
3473
|
+
ancestors.push({
|
|
3474
|
+
testId: tid ?? void 0,
|
|
3475
|
+
dataAttr: did,
|
|
3476
|
+
elementId: pid,
|
|
3477
|
+
className: cls
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
return {
|
|
3482
|
+
testId: testId ?? void 0,
|
|
3483
|
+
role,
|
|
3484
|
+
name,
|
|
3485
|
+
ariaLabel,
|
|
3486
|
+
placeholder,
|
|
3487
|
+
alt,
|
|
3488
|
+
titleAttr,
|
|
3489
|
+
text,
|
|
3490
|
+
href,
|
|
3491
|
+
elementId,
|
|
3492
|
+
nameAttr,
|
|
3493
|
+
dataAttr,
|
|
3494
|
+
tooltipAttr,
|
|
3495
|
+
ancestors
|
|
3496
|
+
};
|
|
3497
|
+
function inferRole(tag2, el2) {
|
|
3498
|
+
if (tag2 === "a" && el2.hasAttribute("href")) return "link";
|
|
3499
|
+
if (tag2 === "button") return "button";
|
|
3500
|
+
if (tag2 === "input") {
|
|
3501
|
+
const type = (el2.getAttribute("type") ?? "text").toLowerCase();
|
|
3502
|
+
if (type === "checkbox") return "checkbox";
|
|
3503
|
+
if (type === "radio") return "radio";
|
|
3504
|
+
if (type === "submit" || type === "button" || type === "reset") return "button";
|
|
3505
|
+
if (type === "range") return "slider";
|
|
3506
|
+
return "textbox";
|
|
3507
|
+
}
|
|
3508
|
+
if (tag2 === "textarea") return "textbox";
|
|
3509
|
+
if (tag2 === "select") return "combobox";
|
|
3510
|
+
if (tag2 === "img") return "img";
|
|
3511
|
+
if (/^h[1-6]$/.test(tag2)) return "heading";
|
|
3512
|
+
if (tag2 === "nav") return "navigation";
|
|
3513
|
+
if (tag2 === "main") return "main";
|
|
3514
|
+
if (tag2 === "header") return "banner";
|
|
3515
|
+
if (tag2 === "footer") return "contentinfo";
|
|
3516
|
+
if (tag2 === "aside") return "complementary";
|
|
3517
|
+
if (tag2 === "form") return "form";
|
|
3518
|
+
return void 0;
|
|
3519
|
+
}
|
|
3520
|
+
__name(inferRole, "inferRole");
|
|
3521
|
+
}
|
|
3522
|
+
__name(inspectElementRaw, "inspectElementRaw");
|
|
3523
|
+
export {
|
|
3524
|
+
ariaSnapshotAlgorithm,
|
|
3525
|
+
findElementAlgorithm,
|
|
3526
|
+
inspectElementRaw,
|
|
3527
|
+
outlineAlgorithm,
|
|
3528
|
+
pasteValue,
|
|
3529
|
+
readOuterHtml,
|
|
3530
|
+
semanticDomAlgorithm,
|
|
3531
|
+
serializeRuntimeDom
|
|
3532
|
+
};
|
|
3533
|
+
//# sourceMappingURL=page-inject.js.map
|