@canivel/ralph 0.2.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/.agents/ralph/PROMPT_build.md +126 -0
- package/.agents/ralph/agents.sh +15 -0
- package/.agents/ralph/config.sh +25 -0
- package/.agents/ralph/log-activity.sh +15 -0
- package/.agents/ralph/loop.sh +1001 -0
- package/.agents/ralph/references/CONTEXT_ENGINEERING.md +126 -0
- package/.agents/ralph/references/GUARDRAILS.md +174 -0
- package/AGENTS.md +20 -0
- package/README.md +266 -0
- package/bin/ralph +766 -0
- package/diagram.svg +55 -0
- package/examples/commands.md +46 -0
- package/package.json +39 -0
- package/ralph.webp +0 -0
- package/skills/commit/SKILL.md +219 -0
- package/skills/commit/references/commit_examples.md +292 -0
- package/skills/dev-browser/SKILL.md +211 -0
- package/skills/dev-browser/bun.lock +443 -0
- package/skills/dev-browser/package-lock.json +2988 -0
- package/skills/dev-browser/package.json +31 -0
- package/skills/dev-browser/references/scraping.md +155 -0
- package/skills/dev-browser/scripts/start-relay.ts +32 -0
- package/skills/dev-browser/scripts/start-server.ts +117 -0
- package/skills/dev-browser/server.sh +24 -0
- package/skills/dev-browser/src/client.ts +474 -0
- package/skills/dev-browser/src/index.ts +287 -0
- package/skills/dev-browser/src/relay.ts +731 -0
- package/skills/dev-browser/src/snapshot/__tests__/snapshot.test.ts +223 -0
- package/skills/dev-browser/src/snapshot/browser-script.ts +877 -0
- package/skills/dev-browser/src/snapshot/index.ts +14 -0
- package/skills/dev-browser/src/snapshot/inject.ts +13 -0
- package/skills/dev-browser/src/types.ts +34 -0
- package/skills/dev-browser/tsconfig.json +36 -0
- package/skills/dev-browser/vitest.config.ts +12 -0
- package/skills/prd/SKILL.md +235 -0
- package/tests/agent-loops.mjs +79 -0
- package/tests/agent-ping.mjs +39 -0
- package/tests/audit.md +56 -0
- package/tests/cli-smoke.mjs +47 -0
- package/tests/real-agents.mjs +127 -0
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-injectable snapshot script.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the snapshot functionality as a string that can be
|
|
5
|
+
* injected into the browser via page.addScriptTag() or page.evaluate().
|
|
6
|
+
*
|
|
7
|
+
* The approach is to read the compiled JavaScript at runtime and bundle it
|
|
8
|
+
* into a single script that exposes window.__devBrowser_getAISnapshot() and
|
|
9
|
+
* window.__devBrowser_selectSnapshotRef().
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
|
|
15
|
+
// Cache the bundled script
|
|
16
|
+
let cachedScript: string | null = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the snapshot script that can be injected into the browser.
|
|
20
|
+
* Returns a self-contained JavaScript string that:
|
|
21
|
+
* 1. Defines all necessary functions (domUtils, roleUtils, yaml, ariaSnapshot)
|
|
22
|
+
* 2. Exposes window.__devBrowser_getAISnapshot()
|
|
23
|
+
* 3. Exposes window.__devBrowser_selectSnapshotRef()
|
|
24
|
+
*/
|
|
25
|
+
export function getSnapshotScript(): string {
|
|
26
|
+
if (cachedScript) return cachedScript;
|
|
27
|
+
|
|
28
|
+
// Read the compiled JavaScript files
|
|
29
|
+
const snapshotDir = path.dirname(new URL(import.meta.url).pathname);
|
|
30
|
+
|
|
31
|
+
// For now, we'll inline the functions directly
|
|
32
|
+
// In production, we could use a bundler like esbuild to create a single file
|
|
33
|
+
cachedScript = `
|
|
34
|
+
(function() {
|
|
35
|
+
// Skip if already injected
|
|
36
|
+
if (window.__devBrowser_getAISnapshot) return;
|
|
37
|
+
|
|
38
|
+
${getDomUtilsCode()}
|
|
39
|
+
${getYamlCode()}
|
|
40
|
+
${getRoleUtilsCode()}
|
|
41
|
+
${getAriaSnapshotCode()}
|
|
42
|
+
|
|
43
|
+
// Expose main functions
|
|
44
|
+
window.__devBrowser_getAISnapshot = getAISnapshot;
|
|
45
|
+
window.__devBrowser_selectSnapshotRef = selectSnapshotRef;
|
|
46
|
+
})();
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
return cachedScript;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getDomUtilsCode(): string {
|
|
53
|
+
return `
|
|
54
|
+
// === domUtils ===
|
|
55
|
+
let cacheStyle;
|
|
56
|
+
let cachesCounter = 0;
|
|
57
|
+
|
|
58
|
+
function beginDOMCaches() {
|
|
59
|
+
++cachesCounter;
|
|
60
|
+
cacheStyle = cacheStyle || new Map();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function endDOMCaches() {
|
|
64
|
+
if (!--cachesCounter) {
|
|
65
|
+
cacheStyle = undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getElementComputedStyle(element, pseudo) {
|
|
70
|
+
const cache = cacheStyle;
|
|
71
|
+
const cacheKey = pseudo ? undefined : element;
|
|
72
|
+
if (cache && cacheKey && cache.has(cacheKey)) return cache.get(cacheKey);
|
|
73
|
+
const style = element.ownerDocument && element.ownerDocument.defaultView
|
|
74
|
+
? element.ownerDocument.defaultView.getComputedStyle(element, pseudo)
|
|
75
|
+
: undefined;
|
|
76
|
+
if (cache && cacheKey) cache.set(cacheKey, style);
|
|
77
|
+
return style;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parentElementOrShadowHost(element) {
|
|
81
|
+
if (element.parentElement) return element.parentElement;
|
|
82
|
+
if (!element.parentNode) return;
|
|
83
|
+
if (element.parentNode.nodeType === 11 && element.parentNode.host)
|
|
84
|
+
return element.parentNode.host;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function enclosingShadowRootOrDocument(element) {
|
|
88
|
+
let node = element;
|
|
89
|
+
while (node.parentNode) node = node.parentNode;
|
|
90
|
+
if (node.nodeType === 11 || node.nodeType === 9)
|
|
91
|
+
return node;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function closestCrossShadow(element, css, scope) {
|
|
95
|
+
while (element) {
|
|
96
|
+
const closest = element.closest(css);
|
|
97
|
+
if (scope && closest !== scope && closest?.contains(scope)) return;
|
|
98
|
+
if (closest) return closest;
|
|
99
|
+
element = enclosingShadowHost(element);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function enclosingShadowHost(element) {
|
|
104
|
+
while (element.parentElement) element = element.parentElement;
|
|
105
|
+
return parentElementOrShadowHost(element);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isElementStyleVisibilityVisible(element, style) {
|
|
109
|
+
style = style || getElementComputedStyle(element);
|
|
110
|
+
if (!style) return true;
|
|
111
|
+
if (style.visibility !== "visible") return false;
|
|
112
|
+
const detailsOrSummary = element.closest("details,summary");
|
|
113
|
+
if (detailsOrSummary !== element && detailsOrSummary?.nodeName === "DETAILS" && !detailsOrSummary.open)
|
|
114
|
+
return false;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function computeBox(element) {
|
|
119
|
+
const style = getElementComputedStyle(element);
|
|
120
|
+
if (!style) return { visible: true, inline: false };
|
|
121
|
+
const cursor = style.cursor;
|
|
122
|
+
if (style.display === "contents") {
|
|
123
|
+
for (let child = element.firstChild; child; child = child.nextSibling) {
|
|
124
|
+
if (child.nodeType === 1 && isElementVisible(child))
|
|
125
|
+
return { visible: true, inline: false, cursor };
|
|
126
|
+
if (child.nodeType === 3 && isVisibleTextNode(child))
|
|
127
|
+
return { visible: true, inline: true, cursor };
|
|
128
|
+
}
|
|
129
|
+
return { visible: false, inline: false, cursor };
|
|
130
|
+
}
|
|
131
|
+
if (!isElementStyleVisibilityVisible(element, style))
|
|
132
|
+
return { cursor, visible: false, inline: false };
|
|
133
|
+
const rect = element.getBoundingClientRect();
|
|
134
|
+
return { rect, cursor, visible: rect.width > 0 && rect.height > 0, inline: style.display === "inline" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isElementVisible(element) {
|
|
138
|
+
return computeBox(element).visible;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isVisibleTextNode(node) {
|
|
142
|
+
const range = node.ownerDocument.createRange();
|
|
143
|
+
range.selectNode(node);
|
|
144
|
+
const rect = range.getBoundingClientRect();
|
|
145
|
+
return rect.width > 0 && rect.height > 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function elementSafeTagName(element) {
|
|
149
|
+
const tagName = element.tagName;
|
|
150
|
+
if (typeof tagName === "string") return tagName.toUpperCase();
|
|
151
|
+
if (element instanceof HTMLFormElement) return "FORM";
|
|
152
|
+
return element.tagName.toUpperCase();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeWhiteSpace(text) {
|
|
156
|
+
return text.split("\\u00A0").map(chunk =>
|
|
157
|
+
chunk.replace(/\\r\\n/g, "\\n").replace(/[\\u200b\\u00ad]/g, "").replace(/\\s\\s*/g, " ")
|
|
158
|
+
).join("\\u00A0").trim();
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getYamlCode(): string {
|
|
164
|
+
return `
|
|
165
|
+
// === yaml ===
|
|
166
|
+
function yamlEscapeKeyIfNeeded(str) {
|
|
167
|
+
if (!yamlStringNeedsQuotes(str)) return str;
|
|
168
|
+
return "'" + str.replace(/'/g, "''") + "'";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function yamlEscapeValueIfNeeded(str) {
|
|
172
|
+
if (!yamlStringNeedsQuotes(str)) return str;
|
|
173
|
+
return '"' + str.replace(/[\\\\"\x00-\\x1f\\x7f-\\x9f]/g, c => {
|
|
174
|
+
switch (c) {
|
|
175
|
+
case "\\\\": return "\\\\\\\\";
|
|
176
|
+
case '"': return '\\\\"';
|
|
177
|
+
case "\\b": return "\\\\b";
|
|
178
|
+
case "\\f": return "\\\\f";
|
|
179
|
+
case "\\n": return "\\\\n";
|
|
180
|
+
case "\\r": return "\\\\r";
|
|
181
|
+
case "\\t": return "\\\\t";
|
|
182
|
+
default:
|
|
183
|
+
const code = c.charCodeAt(0);
|
|
184
|
+
return "\\\\x" + code.toString(16).padStart(2, "0");
|
|
185
|
+
}
|
|
186
|
+
}) + '"';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function yamlStringNeedsQuotes(str) {
|
|
190
|
+
if (str.length === 0) return true;
|
|
191
|
+
if (/^\\s|\\s$/.test(str)) return true;
|
|
192
|
+
if (/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\x9f]/.test(str)) return true;
|
|
193
|
+
if (/^-/.test(str)) return true;
|
|
194
|
+
if (/[\\n:](\\s|$)/.test(str)) return true;
|
|
195
|
+
if (/\\s#/.test(str)) return true;
|
|
196
|
+
if (/[\\n\\r]/.test(str)) return true;
|
|
197
|
+
if (/^[&*\\],?!>|@"'#%]/.test(str)) return true;
|
|
198
|
+
if (/[{}\`]/.test(str)) return true;
|
|
199
|
+
if (/^\\[/.test(str)) return true;
|
|
200
|
+
if (!isNaN(Number(str)) || ["y","n","yes","no","true","false","on","off","null"].includes(str.toLowerCase())) return true;
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getRoleUtilsCode(): string {
|
|
207
|
+
return `
|
|
208
|
+
// === roleUtils ===
|
|
209
|
+
const validRoles = ["alert","alertdialog","application","article","banner","blockquote","button","caption","cell","checkbox","code","columnheader","combobox","complementary","contentinfo","definition","deletion","dialog","directory","document","emphasis","feed","figure","form","generic","grid","gridcell","group","heading","img","insertion","link","list","listbox","listitem","log","main","mark","marquee","math","meter","menu","menubar","menuitem","menuitemcheckbox","menuitemradio","navigation","none","note","option","paragraph","presentation","progressbar","radio","radiogroup","region","row","rowgroup","rowheader","scrollbar","search","searchbox","separator","slider","spinbutton","status","strong","subscript","superscript","switch","tab","table","tablist","tabpanel","term","textbox","time","timer","toolbar","tooltip","tree","treegrid","treeitem"];
|
|
210
|
+
|
|
211
|
+
let cacheAccessibleName;
|
|
212
|
+
let cacheIsHidden;
|
|
213
|
+
let cachePointerEvents;
|
|
214
|
+
let ariaCachesCounter = 0;
|
|
215
|
+
|
|
216
|
+
function beginAriaCaches() {
|
|
217
|
+
beginDOMCaches();
|
|
218
|
+
++ariaCachesCounter;
|
|
219
|
+
cacheAccessibleName = cacheAccessibleName || new Map();
|
|
220
|
+
cacheIsHidden = cacheIsHidden || new Map();
|
|
221
|
+
cachePointerEvents = cachePointerEvents || new Map();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function endAriaCaches() {
|
|
225
|
+
if (!--ariaCachesCounter) {
|
|
226
|
+
cacheAccessibleName = undefined;
|
|
227
|
+
cacheIsHidden = undefined;
|
|
228
|
+
cachePointerEvents = undefined;
|
|
229
|
+
}
|
|
230
|
+
endDOMCaches();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function hasExplicitAccessibleName(e) {
|
|
234
|
+
return e.hasAttribute("aria-label") || e.hasAttribute("aria-labelledby");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const kAncestorPreventingLandmark = "article:not([role]), aside:not([role]), main:not([role]), nav:not([role]), section:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]";
|
|
238
|
+
|
|
239
|
+
const kGlobalAriaAttributes = [
|
|
240
|
+
["aria-atomic", undefined],["aria-busy", undefined],["aria-controls", undefined],["aria-current", undefined],
|
|
241
|
+
["aria-describedby", undefined],["aria-details", undefined],["aria-dropeffect", undefined],["aria-flowto", undefined],
|
|
242
|
+
["aria-grabbed", undefined],["aria-hidden", undefined],["aria-keyshortcuts", undefined],
|
|
243
|
+
["aria-label", ["caption","code","deletion","emphasis","generic","insertion","paragraph","presentation","strong","subscript","superscript"]],
|
|
244
|
+
["aria-labelledby", ["caption","code","deletion","emphasis","generic","insertion","paragraph","presentation","strong","subscript","superscript"]],
|
|
245
|
+
["aria-live", undefined],["aria-owns", undefined],["aria-relevant", undefined],["aria-roledescription", ["generic"]]
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
function hasGlobalAriaAttribute(element, forRole) {
|
|
249
|
+
return kGlobalAriaAttributes.some(([attr, prohibited]) => !prohibited?.includes(forRole || "") && element.hasAttribute(attr));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function hasTabIndex(element) {
|
|
253
|
+
return !Number.isNaN(Number(String(element.getAttribute("tabindex"))));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isFocusable(element) {
|
|
257
|
+
return !isNativelyDisabled(element) && (isNativelyFocusable(element) || hasTabIndex(element));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isNativelyFocusable(element) {
|
|
261
|
+
const tagName = elementSafeTagName(element);
|
|
262
|
+
if (["BUTTON","DETAILS","SELECT","TEXTAREA"].includes(tagName)) return true;
|
|
263
|
+
if (tagName === "A" || tagName === "AREA") return element.hasAttribute("href");
|
|
264
|
+
if (tagName === "INPUT") return !element.hidden;
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isNativelyDisabled(element) {
|
|
269
|
+
const isNativeFormControl = ["BUTTON","INPUT","SELECT","TEXTAREA","OPTION","OPTGROUP"].includes(elementSafeTagName(element));
|
|
270
|
+
return isNativeFormControl && (element.hasAttribute("disabled") || belongsToDisabledFieldSet(element));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function belongsToDisabledFieldSet(element) {
|
|
274
|
+
const fieldSetElement = element?.closest("FIELDSET[DISABLED]");
|
|
275
|
+
if (!fieldSetElement) return false;
|
|
276
|
+
const legendElement = fieldSetElement.querySelector(":scope > LEGEND");
|
|
277
|
+
return !legendElement || !legendElement.contains(element);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const inputTypeToRole = {button:"button",checkbox:"checkbox",image:"button",number:"spinbutton",radio:"radio",range:"slider",reset:"button",submit:"button"};
|
|
281
|
+
|
|
282
|
+
function getIdRefs(element, ref) {
|
|
283
|
+
if (!ref) return [];
|
|
284
|
+
const root = enclosingShadowRootOrDocument(element);
|
|
285
|
+
if (!root) return [];
|
|
286
|
+
try {
|
|
287
|
+
const ids = ref.split(" ").filter(id => !!id);
|
|
288
|
+
const result = [];
|
|
289
|
+
for (const id of ids) {
|
|
290
|
+
const firstElement = root.querySelector("#" + CSS.escape(id));
|
|
291
|
+
if (firstElement && !result.includes(firstElement)) result.push(firstElement);
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
} catch { return []; }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const kImplicitRoleByTagName = {
|
|
298
|
+
A: e => e.hasAttribute("href") ? "link" : null,
|
|
299
|
+
AREA: e => e.hasAttribute("href") ? "link" : null,
|
|
300
|
+
ARTICLE: () => "article", ASIDE: () => "complementary", BLOCKQUOTE: () => "blockquote", BUTTON: () => "button",
|
|
301
|
+
CAPTION: () => "caption", CODE: () => "code", DATALIST: () => "listbox", DD: () => "definition",
|
|
302
|
+
DEL: () => "deletion", DETAILS: () => "group", DFN: () => "term", DIALOG: () => "dialog", DT: () => "term",
|
|
303
|
+
EM: () => "emphasis", FIELDSET: () => "group", FIGURE: () => "figure",
|
|
304
|
+
FOOTER: e => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : "contentinfo",
|
|
305
|
+
FORM: e => hasExplicitAccessibleName(e) ? "form" : null,
|
|
306
|
+
H1: () => "heading", H2: () => "heading", H3: () => "heading", H4: () => "heading", H5: () => "heading", H6: () => "heading",
|
|
307
|
+
HEADER: e => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : "banner",
|
|
308
|
+
HR: () => "separator", HTML: () => "document",
|
|
309
|
+
IMG: e => e.getAttribute("alt") === "" && !e.getAttribute("title") && !hasGlobalAriaAttribute(e) && !hasTabIndex(e) ? "presentation" : "img",
|
|
310
|
+
INPUT: e => {
|
|
311
|
+
const type = e.type.toLowerCase();
|
|
312
|
+
if (type === "search") return e.hasAttribute("list") ? "combobox" : "searchbox";
|
|
313
|
+
if (["email","tel","text","url",""].includes(type)) {
|
|
314
|
+
const list = getIdRefs(e, e.getAttribute("list"))[0];
|
|
315
|
+
return list && elementSafeTagName(list) === "DATALIST" ? "combobox" : "textbox";
|
|
316
|
+
}
|
|
317
|
+
if (type === "hidden") return null;
|
|
318
|
+
if (type === "file") return "button";
|
|
319
|
+
return inputTypeToRole[type] || "textbox";
|
|
320
|
+
},
|
|
321
|
+
INS: () => "insertion", LI: () => "listitem", MAIN: () => "main", MARK: () => "mark", MATH: () => "math",
|
|
322
|
+
MENU: () => "list", METER: () => "meter", NAV: () => "navigation", OL: () => "list", OPTGROUP: () => "group",
|
|
323
|
+
OPTION: () => "option", OUTPUT: () => "status", P: () => "paragraph", PROGRESS: () => "progressbar",
|
|
324
|
+
SEARCH: () => "search", SECTION: e => hasExplicitAccessibleName(e) ? "region" : null,
|
|
325
|
+
SELECT: e => e.hasAttribute("multiple") || e.size > 1 ? "listbox" : "combobox",
|
|
326
|
+
STRONG: () => "strong", SUB: () => "subscript", SUP: () => "superscript", SVG: () => "img",
|
|
327
|
+
TABLE: () => "table", TBODY: () => "rowgroup",
|
|
328
|
+
TD: e => { const table = closestCrossShadow(e, "table"); const role = table ? getExplicitAriaRole(table) : ""; return role === "grid" || role === "treegrid" ? "gridcell" : "cell"; },
|
|
329
|
+
TEXTAREA: () => "textbox", TFOOT: () => "rowgroup",
|
|
330
|
+
TH: e => { const scope = e.getAttribute("scope"); if (scope === "col" || scope === "colgroup") return "columnheader"; if (scope === "row" || scope === "rowgroup") return "rowheader"; return "columnheader"; },
|
|
331
|
+
THEAD: () => "rowgroup", TIME: () => "time", TR: () => "row", UL: () => "list"
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
function getExplicitAriaRole(element) {
|
|
335
|
+
const roles = (element.getAttribute("role") || "").split(" ").map(role => role.trim());
|
|
336
|
+
return roles.find(role => validRoles.includes(role)) || null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function getImplicitAriaRole(element) {
|
|
340
|
+
const fn = kImplicitRoleByTagName[elementSafeTagName(element)];
|
|
341
|
+
return fn ? fn(element) : null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function hasPresentationConflictResolution(element, role) {
|
|
345
|
+
return hasGlobalAriaAttribute(element, role) || isFocusable(element);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function getAriaRole(element) {
|
|
349
|
+
const explicitRole = getExplicitAriaRole(element);
|
|
350
|
+
if (!explicitRole) return getImplicitAriaRole(element);
|
|
351
|
+
if (explicitRole === "none" || explicitRole === "presentation") {
|
|
352
|
+
const implicitRole = getImplicitAriaRole(element);
|
|
353
|
+
if (hasPresentationConflictResolution(element, implicitRole)) return implicitRole;
|
|
354
|
+
}
|
|
355
|
+
return explicitRole;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function getAriaBoolean(attr) {
|
|
359
|
+
return attr === null ? undefined : attr.toLowerCase() === "true";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function isElementIgnoredForAria(element) {
|
|
363
|
+
return ["STYLE","SCRIPT","NOSCRIPT","TEMPLATE"].includes(elementSafeTagName(element));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isElementHiddenForAria(element) {
|
|
367
|
+
if (isElementIgnoredForAria(element)) return true;
|
|
368
|
+
const style = getElementComputedStyle(element);
|
|
369
|
+
const isSlot = element.nodeName === "SLOT";
|
|
370
|
+
if (style?.display === "contents" && !isSlot) {
|
|
371
|
+
for (let child = element.firstChild; child; child = child.nextSibling) {
|
|
372
|
+
if (child.nodeType === 1 && !isElementHiddenForAria(child)) return false;
|
|
373
|
+
if (child.nodeType === 3 && isVisibleTextNode(child)) return false;
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
const isOptionInsideSelect = element.nodeName === "OPTION" && !!element.closest("select");
|
|
378
|
+
if (!isOptionInsideSelect && !isSlot && !isElementStyleVisibilityVisible(element, style)) return true;
|
|
379
|
+
return belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element) {
|
|
383
|
+
let hidden = cacheIsHidden?.get(element);
|
|
384
|
+
if (hidden === undefined) {
|
|
385
|
+
hidden = false;
|
|
386
|
+
if (element.parentElement && element.parentElement.shadowRoot && !element.assignedSlot) hidden = true;
|
|
387
|
+
if (!hidden) {
|
|
388
|
+
const style = getElementComputedStyle(element);
|
|
389
|
+
hidden = !style || style.display === "none" || getAriaBoolean(element.getAttribute("aria-hidden")) === true;
|
|
390
|
+
}
|
|
391
|
+
if (!hidden) {
|
|
392
|
+
const parent = parentElementOrShadowHost(element);
|
|
393
|
+
if (parent) hidden = belongsToDisplayNoneOrAriaHiddenOrNonSlotted(parent);
|
|
394
|
+
}
|
|
395
|
+
cacheIsHidden?.set(element, hidden);
|
|
396
|
+
}
|
|
397
|
+
return hidden;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function getAriaLabelledByElements(element) {
|
|
401
|
+
const ref = element.getAttribute("aria-labelledby");
|
|
402
|
+
if (ref === null) return null;
|
|
403
|
+
const refs = getIdRefs(element, ref);
|
|
404
|
+
return refs.length ? refs : null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function getElementAccessibleName(element, includeHidden) {
|
|
408
|
+
let accessibleName = cacheAccessibleName?.get(element);
|
|
409
|
+
if (accessibleName === undefined) {
|
|
410
|
+
accessibleName = "";
|
|
411
|
+
const elementProhibitsNaming = ["caption","code","definition","deletion","emphasis","generic","insertion","mark","paragraph","presentation","strong","subscript","suggestion","superscript","term","time"].includes(getAriaRole(element) || "");
|
|
412
|
+
if (!elementProhibitsNaming) {
|
|
413
|
+
accessibleName = normalizeWhiteSpace(getTextAlternativeInternal(element, { includeHidden, visitedElements: new Set(), embeddedInTargetElement: "self" }));
|
|
414
|
+
}
|
|
415
|
+
cacheAccessibleName?.set(element, accessibleName);
|
|
416
|
+
}
|
|
417
|
+
return accessibleName;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function getTextAlternativeInternal(element, options) {
|
|
421
|
+
if (options.visitedElements.has(element)) return "";
|
|
422
|
+
const childOptions = { ...options, embeddedInTargetElement: options.embeddedInTargetElement === "self" ? "descendant" : options.embeddedInTargetElement };
|
|
423
|
+
|
|
424
|
+
if (!options.includeHidden) {
|
|
425
|
+
const isEmbeddedInHiddenReferenceTraversal = !!options.embeddedInLabelledBy?.hidden || !!options.embeddedInLabel?.hidden;
|
|
426
|
+
if (isElementIgnoredForAria(element) || (!isEmbeddedInHiddenReferenceTraversal && isElementHiddenForAria(element))) {
|
|
427
|
+
options.visitedElements.add(element);
|
|
428
|
+
return "";
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const labelledBy = getAriaLabelledByElements(element);
|
|
433
|
+
if (!options.embeddedInLabelledBy) {
|
|
434
|
+
const accessibleName = (labelledBy || []).map(ref => getTextAlternativeInternal(ref, { ...options, embeddedInLabelledBy: { element: ref, hidden: isElementHiddenForAria(ref) }, embeddedInTargetElement: undefined, embeddedInLabel: undefined })).join(" ");
|
|
435
|
+
if (accessibleName) return accessibleName;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const role = getAriaRole(element) || "";
|
|
439
|
+
const tagName = elementSafeTagName(element);
|
|
440
|
+
|
|
441
|
+
const ariaLabel = element.getAttribute("aria-label") || "";
|
|
442
|
+
if (ariaLabel.trim()) { options.visitedElements.add(element); return ariaLabel; }
|
|
443
|
+
|
|
444
|
+
if (!["presentation","none"].includes(role)) {
|
|
445
|
+
if (tagName === "INPUT" && ["button","submit","reset"].includes(element.type)) {
|
|
446
|
+
options.visitedElements.add(element);
|
|
447
|
+
const value = element.value || "";
|
|
448
|
+
if (value.trim()) return value;
|
|
449
|
+
if (element.type === "submit") return "Submit";
|
|
450
|
+
if (element.type === "reset") return "Reset";
|
|
451
|
+
return element.getAttribute("title") || "";
|
|
452
|
+
}
|
|
453
|
+
if (tagName === "INPUT" && element.type === "image") {
|
|
454
|
+
options.visitedElements.add(element);
|
|
455
|
+
const alt = element.getAttribute("alt") || "";
|
|
456
|
+
if (alt.trim()) return alt;
|
|
457
|
+
const title = element.getAttribute("title") || "";
|
|
458
|
+
if (title.trim()) return title;
|
|
459
|
+
return "Submit";
|
|
460
|
+
}
|
|
461
|
+
if (tagName === "IMG") {
|
|
462
|
+
options.visitedElements.add(element);
|
|
463
|
+
const alt = element.getAttribute("alt") || "";
|
|
464
|
+
if (alt.trim()) return alt;
|
|
465
|
+
return element.getAttribute("title") || "";
|
|
466
|
+
}
|
|
467
|
+
if (!labelledBy && ["BUTTON","INPUT","TEXTAREA","SELECT"].includes(tagName)) {
|
|
468
|
+
const labels = element.labels;
|
|
469
|
+
if (labels?.length) {
|
|
470
|
+
options.visitedElements.add(element);
|
|
471
|
+
return [...labels].map(label => getTextAlternativeInternal(label, { ...options, embeddedInLabel: { element: label, hidden: isElementHiddenForAria(label) }, embeddedInLabelledBy: undefined, embeddedInTargetElement: undefined })).filter(name => !!name).join(" ");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const allowsNameFromContent = ["button","cell","checkbox","columnheader","gridcell","heading","link","menuitem","menuitemcheckbox","menuitemradio","option","radio","row","rowheader","switch","tab","tooltip","treeitem"].includes(role);
|
|
477
|
+
if (allowsNameFromContent || !!options.embeddedInLabelledBy || !!options.embeddedInLabel) {
|
|
478
|
+
options.visitedElements.add(element);
|
|
479
|
+
const accessibleName = innerAccumulatedElementText(element, childOptions);
|
|
480
|
+
const maybeTrimmedAccessibleName = options.embeddedInTargetElement === "self" ? accessibleName.trim() : accessibleName;
|
|
481
|
+
if (maybeTrimmedAccessibleName) return accessibleName;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!["presentation","none"].includes(role) || tagName === "IFRAME") {
|
|
485
|
+
options.visitedElements.add(element);
|
|
486
|
+
const title = element.getAttribute("title") || "";
|
|
487
|
+
if (title.trim()) return title;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
options.visitedElements.add(element);
|
|
491
|
+
return "";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function innerAccumulatedElementText(element, options) {
|
|
495
|
+
const tokens = [];
|
|
496
|
+
const visit = (node, skipSlotted) => {
|
|
497
|
+
if (skipSlotted && node.assignedSlot) return;
|
|
498
|
+
if (node.nodeType === 1) {
|
|
499
|
+
const display = getElementComputedStyle(node)?.display || "inline";
|
|
500
|
+
let token = getTextAlternativeInternal(node, options);
|
|
501
|
+
if (display !== "inline" || node.nodeName === "BR") token = " " + token + " ";
|
|
502
|
+
tokens.push(token);
|
|
503
|
+
} else if (node.nodeType === 3) {
|
|
504
|
+
tokens.push(node.textContent || "");
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
const assignedNodes = element.nodeName === "SLOT" ? element.assignedNodes() : [];
|
|
508
|
+
if (assignedNodes.length) {
|
|
509
|
+
for (const child of assignedNodes) visit(child, false);
|
|
510
|
+
} else {
|
|
511
|
+
for (let child = element.firstChild; child; child = child.nextSibling) visit(child, true);
|
|
512
|
+
if (element.shadowRoot) {
|
|
513
|
+
for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) visit(child, true);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return tokens.join("");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const kAriaCheckedRoles = ["checkbox","menuitemcheckbox","option","radio","switch","menuitemradio","treeitem"];
|
|
520
|
+
function getAriaChecked(element) {
|
|
521
|
+
const tagName = elementSafeTagName(element);
|
|
522
|
+
if (tagName === "INPUT" && element.indeterminate) return "mixed";
|
|
523
|
+
if (tagName === "INPUT" && ["checkbox","radio"].includes(element.type)) return element.checked;
|
|
524
|
+
if (kAriaCheckedRoles.includes(getAriaRole(element) || "")) {
|
|
525
|
+
const checked = element.getAttribute("aria-checked");
|
|
526
|
+
if (checked === "true") return true;
|
|
527
|
+
if (checked === "mixed") return "mixed";
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const kAriaDisabledRoles = ["application","button","composite","gridcell","group","input","link","menuitem","scrollbar","separator","tab","checkbox","columnheader","combobox","grid","listbox","menu","menubar","menuitemcheckbox","menuitemradio","option","radio","radiogroup","row","rowheader","searchbox","select","slider","spinbutton","switch","tablist","textbox","toolbar","tree","treegrid","treeitem"];
|
|
534
|
+
function getAriaDisabled(element) {
|
|
535
|
+
return isNativelyDisabled(element) || hasExplicitAriaDisabled(element);
|
|
536
|
+
}
|
|
537
|
+
function hasExplicitAriaDisabled(element, isAncestor) {
|
|
538
|
+
if (!element) return false;
|
|
539
|
+
if (isAncestor || kAriaDisabledRoles.includes(getAriaRole(element) || "")) {
|
|
540
|
+
const attribute = (element.getAttribute("aria-disabled") || "").toLowerCase();
|
|
541
|
+
if (attribute === "true") return true;
|
|
542
|
+
if (attribute === "false") return false;
|
|
543
|
+
return hasExplicitAriaDisabled(parentElementOrShadowHost(element), true);
|
|
544
|
+
}
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const kAriaExpandedRoles = ["application","button","checkbox","combobox","gridcell","link","listbox","menuitem","row","rowheader","tab","treeitem","columnheader","menuitemcheckbox","menuitemradio","switch"];
|
|
549
|
+
function getAriaExpanded(element) {
|
|
550
|
+
if (elementSafeTagName(element) === "DETAILS") return element.open;
|
|
551
|
+
if (kAriaExpandedRoles.includes(getAriaRole(element) || "")) {
|
|
552
|
+
const expanded = element.getAttribute("aria-expanded");
|
|
553
|
+
if (expanded === null) return undefined;
|
|
554
|
+
if (expanded === "true") return true;
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const kAriaLevelRoles = ["heading","listitem","row","treeitem"];
|
|
561
|
+
function getAriaLevel(element) {
|
|
562
|
+
const native = {H1:1,H2:2,H3:3,H4:4,H5:5,H6:6}[elementSafeTagName(element)];
|
|
563
|
+
if (native) return native;
|
|
564
|
+
if (kAriaLevelRoles.includes(getAriaRole(element) || "")) {
|
|
565
|
+
const attr = element.getAttribute("aria-level");
|
|
566
|
+
const value = attr === null ? Number.NaN : Number(attr);
|
|
567
|
+
if (Number.isInteger(value) && value >= 1) return value;
|
|
568
|
+
}
|
|
569
|
+
return 0;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const kAriaPressedRoles = ["button"];
|
|
573
|
+
function getAriaPressed(element) {
|
|
574
|
+
if (kAriaPressedRoles.includes(getAriaRole(element) || "")) {
|
|
575
|
+
const pressed = element.getAttribute("aria-pressed");
|
|
576
|
+
if (pressed === "true") return true;
|
|
577
|
+
if (pressed === "mixed") return "mixed";
|
|
578
|
+
}
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const kAriaSelectedRoles = ["gridcell","option","row","tab","rowheader","columnheader","treeitem"];
|
|
583
|
+
function getAriaSelected(element) {
|
|
584
|
+
if (elementSafeTagName(element) === "OPTION") return element.selected;
|
|
585
|
+
if (kAriaSelectedRoles.includes(getAriaRole(element) || "")) return getAriaBoolean(element.getAttribute("aria-selected")) === true;
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function receivesPointerEvents(element) {
|
|
590
|
+
const cache = cachePointerEvents;
|
|
591
|
+
let e = element;
|
|
592
|
+
let result;
|
|
593
|
+
const parents = [];
|
|
594
|
+
for (; e; e = parentElementOrShadowHost(e)) {
|
|
595
|
+
const cached = cache?.get(e);
|
|
596
|
+
if (cached !== undefined) { result = cached; break; }
|
|
597
|
+
parents.push(e);
|
|
598
|
+
const style = getElementComputedStyle(e);
|
|
599
|
+
if (!style) { result = true; break; }
|
|
600
|
+
const value = style.pointerEvents;
|
|
601
|
+
if (value) { result = value !== "none"; break; }
|
|
602
|
+
}
|
|
603
|
+
if (result === undefined) result = true;
|
|
604
|
+
for (const parent of parents) cache?.set(parent, result);
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function getCSSContent(element, pseudo) {
|
|
609
|
+
const style = getElementComputedStyle(element, pseudo);
|
|
610
|
+
if (!style) return undefined;
|
|
611
|
+
const contentValue = style.content;
|
|
612
|
+
if (!contentValue || contentValue === "none" || contentValue === "normal") return undefined;
|
|
613
|
+
if (style.display === "none" || style.visibility === "hidden") return undefined;
|
|
614
|
+
const match = contentValue.match(/^"(.*)"$/);
|
|
615
|
+
if (match) {
|
|
616
|
+
const content = match[1].replace(/\\\\"/g, '"');
|
|
617
|
+
if (pseudo) {
|
|
618
|
+
const display = style.display || "inline";
|
|
619
|
+
if (display !== "inline") return " " + content + " ";
|
|
620
|
+
}
|
|
621
|
+
return content;
|
|
622
|
+
}
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function getAriaSnapshotCode(): string {
|
|
629
|
+
return `
|
|
630
|
+
// === ariaSnapshot ===
|
|
631
|
+
let lastRef = 0;
|
|
632
|
+
|
|
633
|
+
function generateAriaTree(rootElement) {
|
|
634
|
+
const options = { visibility: "ariaOrVisible", refs: "interactable", refPrefix: "", includeGenericRole: true, renderActive: true, renderCursorPointer: true };
|
|
635
|
+
const visited = new Set();
|
|
636
|
+
const snapshot = {
|
|
637
|
+
root: { role: "fragment", name: "", children: [], element: rootElement, props: {}, box: computeBox(rootElement), receivesPointerEvents: true },
|
|
638
|
+
elements: new Map(),
|
|
639
|
+
refs: new Map(),
|
|
640
|
+
iframeRefs: []
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const visit = (ariaNode, node, parentElementVisible) => {
|
|
644
|
+
if (visited.has(node)) return;
|
|
645
|
+
visited.add(node);
|
|
646
|
+
if (node.nodeType === Node.TEXT_NODE && node.nodeValue) {
|
|
647
|
+
if (!parentElementVisible) return;
|
|
648
|
+
const text = node.nodeValue;
|
|
649
|
+
if (ariaNode.role !== "textbox" && text) ariaNode.children.push(node.nodeValue || "");
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
653
|
+
const element = node;
|
|
654
|
+
const isElementVisibleForAria = !isElementHiddenForAria(element);
|
|
655
|
+
let visible = isElementVisibleForAria;
|
|
656
|
+
if (options.visibility === "ariaOrVisible") visible = isElementVisibleForAria || isElementVisible(element);
|
|
657
|
+
if (options.visibility === "ariaAndVisible") visible = isElementVisibleForAria && isElementVisible(element);
|
|
658
|
+
if (options.visibility === "aria" && !visible) return;
|
|
659
|
+
const ariaChildren = [];
|
|
660
|
+
if (element.hasAttribute("aria-owns")) {
|
|
661
|
+
const ids = element.getAttribute("aria-owns").split(/\\s+/);
|
|
662
|
+
for (const id of ids) {
|
|
663
|
+
const ownedElement = rootElement.ownerDocument.getElementById(id);
|
|
664
|
+
if (ownedElement) ariaChildren.push(ownedElement);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const childAriaNode = visible ? toAriaNode(element, options) : null;
|
|
668
|
+
if (childAriaNode) {
|
|
669
|
+
if (childAriaNode.ref) {
|
|
670
|
+
snapshot.elements.set(childAriaNode.ref, element);
|
|
671
|
+
snapshot.refs.set(element, childAriaNode.ref);
|
|
672
|
+
if (childAriaNode.role === "iframe") snapshot.iframeRefs.push(childAriaNode.ref);
|
|
673
|
+
}
|
|
674
|
+
ariaNode.children.push(childAriaNode);
|
|
675
|
+
}
|
|
676
|
+
processElement(childAriaNode || ariaNode, element, ariaChildren, visible);
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
function processElement(ariaNode, element, ariaChildren, parentElementVisible) {
|
|
680
|
+
const display = getElementComputedStyle(element)?.display || "inline";
|
|
681
|
+
const treatAsBlock = display !== "inline" || element.nodeName === "BR" ? " " : "";
|
|
682
|
+
if (treatAsBlock) ariaNode.children.push(treatAsBlock);
|
|
683
|
+
ariaNode.children.push(getCSSContent(element, "::before") || "");
|
|
684
|
+
const assignedNodes = element.nodeName === "SLOT" ? element.assignedNodes() : [];
|
|
685
|
+
if (assignedNodes.length) {
|
|
686
|
+
for (const child of assignedNodes) visit(ariaNode, child, parentElementVisible);
|
|
687
|
+
} else {
|
|
688
|
+
for (let child = element.firstChild; child; child = child.nextSibling) {
|
|
689
|
+
if (!child.assignedSlot) visit(ariaNode, child, parentElementVisible);
|
|
690
|
+
}
|
|
691
|
+
if (element.shadowRoot) {
|
|
692
|
+
for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) visit(ariaNode, child, parentElementVisible);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
for (const child of ariaChildren) visit(ariaNode, child, parentElementVisible);
|
|
696
|
+
ariaNode.children.push(getCSSContent(element, "::after") || "");
|
|
697
|
+
if (treatAsBlock) ariaNode.children.push(treatAsBlock);
|
|
698
|
+
if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0]) ariaNode.children = [];
|
|
699
|
+
if (ariaNode.role === "link" && element.hasAttribute("href")) ariaNode.props["url"] = element.getAttribute("href");
|
|
700
|
+
if (ariaNode.role === "textbox" && element.hasAttribute("placeholder") && element.getAttribute("placeholder") !== ariaNode.name) ariaNode.props["placeholder"] = element.getAttribute("placeholder");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
beginAriaCaches();
|
|
704
|
+
try { visit(snapshot.root, rootElement, true); }
|
|
705
|
+
finally { endAriaCaches(); }
|
|
706
|
+
normalizeStringChildren(snapshot.root);
|
|
707
|
+
normalizeGenericRoles(snapshot.root);
|
|
708
|
+
return snapshot;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function computeAriaRef(ariaNode, options) {
|
|
712
|
+
if (options.refs === "none") return;
|
|
713
|
+
if (options.refs === "interactable" && (!ariaNode.box.visible || !ariaNode.receivesPointerEvents)) return;
|
|
714
|
+
let ariaRef = ariaNode.element._ariaRef;
|
|
715
|
+
if (!ariaRef || ariaRef.role !== ariaNode.role || ariaRef.name !== ariaNode.name) {
|
|
716
|
+
ariaRef = { role: ariaNode.role, name: ariaNode.name, ref: (options.refPrefix || "") + "e" + (++lastRef) };
|
|
717
|
+
ariaNode.element._ariaRef = ariaRef;
|
|
718
|
+
}
|
|
719
|
+
ariaNode.ref = ariaRef.ref;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function toAriaNode(element, options) {
|
|
723
|
+
const active = element.ownerDocument.activeElement === element;
|
|
724
|
+
if (element.nodeName === "IFRAME") {
|
|
725
|
+
const ariaNode = { role: "iframe", name: "", children: [], props: {}, element, box: computeBox(element), receivesPointerEvents: true, active };
|
|
726
|
+
computeAriaRef(ariaNode, options);
|
|
727
|
+
return ariaNode;
|
|
728
|
+
}
|
|
729
|
+
const defaultRole = options.includeGenericRole ? "generic" : null;
|
|
730
|
+
const role = getAriaRole(element) || defaultRole;
|
|
731
|
+
if (!role || role === "presentation" || role === "none") return null;
|
|
732
|
+
const name = normalizeWhiteSpace(getElementAccessibleName(element, false) || "");
|
|
733
|
+
const receivesPointerEventsValue = receivesPointerEvents(element);
|
|
734
|
+
const box = computeBox(element);
|
|
735
|
+
if (role === "generic" && box.inline && element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE) return null;
|
|
736
|
+
const result = { role, name, children: [], props: {}, element, box, receivesPointerEvents: receivesPointerEventsValue, active };
|
|
737
|
+
computeAriaRef(result, options);
|
|
738
|
+
if (kAriaCheckedRoles.includes(role)) result.checked = getAriaChecked(element);
|
|
739
|
+
if (kAriaDisabledRoles.includes(role)) result.disabled = getAriaDisabled(element);
|
|
740
|
+
if (kAriaExpandedRoles.includes(role)) result.expanded = getAriaExpanded(element);
|
|
741
|
+
if (kAriaLevelRoles.includes(role)) result.level = getAriaLevel(element);
|
|
742
|
+
if (kAriaPressedRoles.includes(role)) result.pressed = getAriaPressed(element);
|
|
743
|
+
if (kAriaSelectedRoles.includes(role)) result.selected = getAriaSelected(element);
|
|
744
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
745
|
+
if (element.type !== "checkbox" && element.type !== "radio" && element.type !== "file") result.children = [element.value];
|
|
746
|
+
}
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function normalizeGenericRoles(node) {
|
|
751
|
+
const normalizeChildren = (node) => {
|
|
752
|
+
const result = [];
|
|
753
|
+
for (const child of node.children || []) {
|
|
754
|
+
if (typeof child === "string") { result.push(child); continue; }
|
|
755
|
+
const normalized = normalizeChildren(child);
|
|
756
|
+
result.push(...normalized);
|
|
757
|
+
}
|
|
758
|
+
const removeSelf = node.role === "generic" && !node.name && result.length <= 1 && result.every(c => typeof c !== "string" && !!c.ref);
|
|
759
|
+
if (removeSelf) return result;
|
|
760
|
+
node.children = result;
|
|
761
|
+
return [node];
|
|
762
|
+
};
|
|
763
|
+
normalizeChildren(node);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function normalizeStringChildren(rootA11yNode) {
|
|
767
|
+
const flushChildren = (buffer, normalizedChildren) => {
|
|
768
|
+
if (!buffer.length) return;
|
|
769
|
+
const text = normalizeWhiteSpace(buffer.join(""));
|
|
770
|
+
if (text) normalizedChildren.push(text);
|
|
771
|
+
buffer.length = 0;
|
|
772
|
+
};
|
|
773
|
+
const visit = (ariaNode) => {
|
|
774
|
+
const normalizedChildren = [];
|
|
775
|
+
const buffer = [];
|
|
776
|
+
for (const child of ariaNode.children || []) {
|
|
777
|
+
if (typeof child === "string") { buffer.push(child); }
|
|
778
|
+
else { flushChildren(buffer, normalizedChildren); visit(child); normalizedChildren.push(child); }
|
|
779
|
+
}
|
|
780
|
+
flushChildren(buffer, normalizedChildren);
|
|
781
|
+
ariaNode.children = normalizedChildren.length ? normalizedChildren : [];
|
|
782
|
+
if (ariaNode.children.length === 1 && ariaNode.children[0] === ariaNode.name) ariaNode.children = [];
|
|
783
|
+
};
|
|
784
|
+
visit(rootA11yNode);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function hasPointerCursor(ariaNode) { return ariaNode.box.cursor === "pointer"; }
|
|
788
|
+
|
|
789
|
+
function renderAriaTree(ariaSnapshot) {
|
|
790
|
+
const options = { visibility: "ariaOrVisible", refs: "interactable", refPrefix: "", includeGenericRole: true, renderActive: true, renderCursorPointer: true };
|
|
791
|
+
const lines = [];
|
|
792
|
+
let nodesToRender = ariaSnapshot.root.role === "fragment" ? ariaSnapshot.root.children : [ariaSnapshot.root];
|
|
793
|
+
|
|
794
|
+
const visitText = (text, indent) => {
|
|
795
|
+
const escaped = yamlEscapeValueIfNeeded(text);
|
|
796
|
+
if (escaped) lines.push(indent + "- text: " + escaped);
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const createKey = (ariaNode, renderCursorPointer) => {
|
|
800
|
+
let key = ariaNode.role;
|
|
801
|
+
if (ariaNode.name && ariaNode.name.length <= 900) {
|
|
802
|
+
const name = ariaNode.name;
|
|
803
|
+
if (name) {
|
|
804
|
+
const stringifiedName = name.startsWith("/") && name.endsWith("/") ? name : JSON.stringify(name);
|
|
805
|
+
key += " " + stringifiedName;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (ariaNode.checked === "mixed") key += " [checked=mixed]";
|
|
809
|
+
if (ariaNode.checked === true) key += " [checked]";
|
|
810
|
+
if (ariaNode.disabled) key += " [disabled]";
|
|
811
|
+
if (ariaNode.expanded) key += " [expanded]";
|
|
812
|
+
if (ariaNode.active && options.renderActive) key += " [active]";
|
|
813
|
+
if (ariaNode.level) key += " [level=" + ariaNode.level + "]";
|
|
814
|
+
if (ariaNode.pressed === "mixed") key += " [pressed=mixed]";
|
|
815
|
+
if (ariaNode.pressed === true) key += " [pressed]";
|
|
816
|
+
if (ariaNode.selected === true) key += " [selected]";
|
|
817
|
+
if (ariaNode.ref) {
|
|
818
|
+
key += " [ref=" + ariaNode.ref + "]";
|
|
819
|
+
if (renderCursorPointer && hasPointerCursor(ariaNode)) key += " [cursor=pointer]";
|
|
820
|
+
}
|
|
821
|
+
return key;
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const getSingleInlinedTextChild = (ariaNode) => {
|
|
825
|
+
return ariaNode?.children.length === 1 && typeof ariaNode.children[0] === "string" && !Object.keys(ariaNode.props).length ? ariaNode.children[0] : undefined;
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
const visit = (ariaNode, indent, renderCursorPointer) => {
|
|
829
|
+
const escapedKey = indent + "- " + yamlEscapeKeyIfNeeded(createKey(ariaNode, renderCursorPointer));
|
|
830
|
+
const singleInlinedTextChild = getSingleInlinedTextChild(ariaNode);
|
|
831
|
+
if (!ariaNode.children.length && !Object.keys(ariaNode.props).length) {
|
|
832
|
+
lines.push(escapedKey);
|
|
833
|
+
} else if (singleInlinedTextChild !== undefined) {
|
|
834
|
+
lines.push(escapedKey + ": " + yamlEscapeValueIfNeeded(singleInlinedTextChild));
|
|
835
|
+
} else {
|
|
836
|
+
lines.push(escapedKey + ":");
|
|
837
|
+
for (const [name, value] of Object.entries(ariaNode.props)) lines.push(indent + " - /" + name + ": " + yamlEscapeValueIfNeeded(value));
|
|
838
|
+
const childIndent = indent + " ";
|
|
839
|
+
const inCursorPointer = !!ariaNode.ref && renderCursorPointer && hasPointerCursor(ariaNode);
|
|
840
|
+
for (const child of ariaNode.children) {
|
|
841
|
+
if (typeof child === "string") visitText(child, childIndent);
|
|
842
|
+
else visit(child, childIndent, renderCursorPointer && !inCursorPointer);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
for (const nodeToRender of nodesToRender) {
|
|
848
|
+
if (typeof nodeToRender === "string") visitText(nodeToRender, "");
|
|
849
|
+
else visit(nodeToRender, "", !!options.renderCursorPointer);
|
|
850
|
+
}
|
|
851
|
+
return lines.join("\\n");
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function getAISnapshot() {
|
|
855
|
+
const snapshot = generateAriaTree(document.body);
|
|
856
|
+
const refsObject = {};
|
|
857
|
+
for (const [ref, element] of snapshot.elements) refsObject[ref] = element;
|
|
858
|
+
window.__devBrowserRefs = refsObject;
|
|
859
|
+
return renderAriaTree(snapshot);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function selectSnapshotRef(ref) {
|
|
863
|
+
const refs = window.__devBrowserRefs;
|
|
864
|
+
if (!refs) throw new Error("No snapshot refs found. Call getAISnapshot first.");
|
|
865
|
+
const element = refs[ref];
|
|
866
|
+
if (!element) throw new Error('Ref "' + ref + '" not found. Available refs: ' + Object.keys(refs).join(", "));
|
|
867
|
+
return element;
|
|
868
|
+
}
|
|
869
|
+
`;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Clear the cached script (useful for development/testing)
|
|
874
|
+
*/
|
|
875
|
+
export function clearSnapshotScriptCache(): void {
|
|
876
|
+
cachedScript = null;
|
|
877
|
+
}
|