forked from WebKit/WebKit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathaccessibility-helper.js
373 lines (314 loc) · 14 KB
/
accessibility-helper.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
function axDebug(msg)
{
getOrCreate("console", "div").innerText += `${msg}\n`;
};
// This function is necessary when printing AX attributes that are stringified with angle brackets:
// AXChildren: <array of size 0>
// `debug` outputs to the `innerHTML` of a generated element, so these brackets must be escaped to be printed.
function debugEscaped(message) {
debug(escapeHTML(message));
}
// Dumps the AX table by using the table cellForColumnAndRow API (which is what some ATs use).
function dumpAXTable(axElement, options) {
let output = "";
const rowCount = axElement.rowCount;
const columnCount = axElement.columnCount;
for (let row = 0; row < rowCount; row++) {
for (let column = 0; column < columnCount; column++)
output += `#${axElement.domIdentifier} cellForColumnAndRow(${column}, ${row}).domIdentifier is ${axElement.cellForColumnAndRow(column, row).domIdentifier}\n`;
}
return output;
}
// Dumps the result of a traversal via the UI element search API (which is what some ATs use).
// `options` is an object with these keys:
// * `excludeRoles`: Array of strings representing roles you don't want to include in the output.
// Case insensitive, partial match is fine, e.g. "scrollbar" will exclude "AXScrollBar".
function dumpAXSearchTraversal(axElement, options = { }) {
let output = "";
let searchResult = null;
while (true) {
searchResult = axElement.uiElementForSearchPredicate(searchResult, true, "AXAnyTypeSearchKey", "", false);
if (!searchResult)
break;
const role = searchResult.role;
let excluded = false;
if (Array.isArray(options?.excludeRoles)) {
for (const excludedRole of options.excludeRoles) {
if (role.toLowerCase().includes(excludedRole.toLowerCase())) {
excluded = true;
break;
}
}
}
if (excluded)
continue;
const id = searchResult.domIdentifier;
let resultDescription = `${id ? `#${id} ` : ""}${role}`;
if (role.includes("StaticText"))
resultDescription += ` ${accessibilityController.platformName === "ios" ? searchResult.description : searchResult.stringValue}`;
output += `\n{${resultDescription}}\n`;
}
return output;
}
// Dumps the accessibility tree hierarchy for the given accessibilityObject into
// an element with id="tree", e.g., <pre id="tree"></pre>. In addition, it
// returns a two element array with the first element [0] being false if the
// traversal of the tree was stopped at the stopElement, and second element [1],
// the string representing the accessibility tree.
function dumpAccessibilityTree(accessibilityObject, stopElement, indent, allAttributesIfNeeded, getValueFromTitle, includeSubrole) {
var str = "";
var i = 0;
for (i = 0; i < indent; i++)
str += " ";
str += accessibilityObject.role;
if (includeSubrole === true && accessibilityObject.subrole)
str += " " + accessibilityObject.subrole;
str += " " + (getValueFromTitle === true ? accessibilityObject.title : accessibilityController.platformName === "ios" ? accessibilityObject.description : accessibilityObject.stringValue);
str += allAttributesIfNeeded && accessibilityObject.role == '' ? accessibilityObject.allAttributes() : '';
str += "\n";
var outputTree = document.getElementById("tree");
if (outputTree)
outputTree.innerText += str;
if (stopElement && stopElement.isEqual(accessibilityObject))
return [false, str];
var count = accessibilityObject.childrenCount;
for (i = 0; i < count; ++i) {
childRet = dumpAccessibilityTree(accessibilityObject.childAtIndex(i), stopElement, indent + 1, allAttributesIfNeeded, getValueFromTitle, includeSubrole);
if (!childRet[0])
return [false, str];
str += childRet[1];
}
return [true, str];
}
function touchAccessibilityTree(accessibilityObject) {
var count = accessibilityObject.childrenCount;
for (var i = 0; i < count; ++i) {
if (!touchAccessibilityTree(accessibilityObject.childAtIndex(i)))
return false;
}
return true;
}
function visibleRange(axElement, {width, height, scrollTop}) {
document.body.scrollTop = scrollTop;
testRunner.setViewSize(width, height);
return `Range with view ${width}x${height}, scrollTop ${scrollTop}: ${axElement.stringDescriptionOfAttributeValue("AXVisibleCharacterRange")}\n`;
}
function platformValueForW3CName(accessibilityObject, includeSource=false) {
var result;
if (accessibilityController.platformName == "atspi")
result = accessibilityObject.title
else
result = accessibilityObject.description
if (!includeSource) {
var splitResult = result.split(": ");
return splitResult[1];
}
return result;
}
function platformValueForW3CDescription(accessibilityObject, includeSource=false) {
var result;
if (accessibilityController.platformName == "atspi")
result = accessibilityObject.description
else
result = accessibilityObject.helpText;
if (!includeSource) {
var splitResult = result.split(": ");
return splitResult[1];
}
return result;
}
function platformTextAlternatives(accessibilityObject, includeTitleUIElement=false) {
if (!accessibilityObject)
return "Element not exposed";
if (accessibilityController.platformName === "ios")
return `\t${accessibilityObject.description}`;
result = "\t" + accessibilityObject.title + "\n\t" + accessibilityObject.description;
if (accessibilityController.platformName == "mac")
result += "\n\t" + accessibilityObject.helpText;
if (includeTitleUIElement)
result += "\n\tAXTitleUIElement: " + (accessibilityObject.titleUIElement() ? "non-null" : "null");
return result;
}
function platformRoleForComboBox() {
return accessibilityController.platformName == "atspi" ? "AXRole: AXComboBox" : "AXRole: AXPopUpButton";
}
function platformRoleForStaticText() {
return accessibilityController.platformName == "atspi" ? "AXRole: AXStatic" : "AXRole: AXStaticText";
}
function spinnerForTextInput(accessibilityObject) {
var index = accessibilityController.platformName == "atspi" ? 0 : 1;
return accessibilityObject.childAtIndex(index);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function waitFor(condition)
{
return new Promise((resolve, reject) => {
let interval = setInterval(() => {
if (condition()) {
clearInterval(interval);
resolve();
}
}, 0);
});
}
async function waitForElementById(id) {
let element;
await waitFor(() => {
element = accessibilityController.accessibleElementById(id);
return element;
});
return element;
}
// Executes the operation and waits until an accessibility notification of the provided
// `notificationName` is received. A notification listener is added to the AccessibilityUIElement
// passed in; before the operation is executed. The `operation` is expected to be a function,
// which can optionally be async.
async function waitForNotification(accessibilityElement, notificationName, operation) {
var reached = false;
function listener(receivedNotification) {
if (receivedNotification == notificationName) {
reached = true;
accessibilityElement.removeNotificationListener(listener);
}
}
accessibilityElement.addNotificationListener(listener);
await operation();
await waitFor(() => { return reached; });
}
// Expect an expression to equal a value and return the result as a string.
// This is essentially the more ubiquitous `shouldBe` function from js-test,
// but returns the result as a string rather than `debug`ing to a console DOM element.
function expect(expression, expectedValue) {
if (typeof expression !== "string")
debug("WARN: The expression arg in expect() should be a string.");
const evalExpression = `${expression} === ${expectedValue}`;
if (eval(evalExpression))
return `PASS: ${evalExpression}\n`;
return `FAIL: ${expression} !== ${expectedValue}, was ${eval(expression)}\n`;
}
function expectRectWithVariance(expression, x, y, width, height, allowedVariance) {
if (typeof expression !== "string")
debug("WARN: The expression arg in expectRectWithVariance() must be a string.");
if (typeof x !== "number" || typeof y !== "number" || typeof width !== "number" || typeof height !== "number" || typeof allowedVariance !== "number")
debug("WARN: The x, y, width, height, and allowedVariance arguments in expectRectWithVariance must be numbers.");
const expectedRect = `(x: ${x}, y: ${y}, w: ${width}, h: ${height})`;
const result = eval(expression);
const parsedResult = result
.replaceAll(/{|}/g, '') // Eliminate curly braces because they break parseFloat.
.split(/[ ,]+/) // Split on whitespace and commas.
.map(token => parseFloat(token))
.filter(float => !isNaN(float));
if (parsedResult.length !== 4)
debug(`FAIL: Expression ${expression} didn't produce a string result with four numbers (was ${result}).\n`);
else if (Math.abs(x - parsedResult[0]) > allowedVariance ||
Math.abs(y - parsedResult[1]) > allowedVariance ||
Math.abs(width - parsedResult[2]) > allowedVariance ||
Math.abs(height - parsedResult[3]) > allowedVariance)
return `FAIL: ${expression} varied more than allowed variance ${allowedVariance}. Was: ${result}, expected ${expectedRect}\n`;
else
return `PASS: ${expression} was ${allowedVariance === 0 ? "equal" : "equal or approximately equal"} to ${expectedRect}.\n`;
}
async function expectAsync(expression, expectedValue) {
if (typeof expression !== "string")
debug("WARN: The expression arg in expectAsync should be a string.");
const evalExpression = `${expression} === ${expectedValue}`;
await waitFor(() => {
return eval(evalExpression);
});
return `PASS: ${evalExpression}\n`;
}
async function expectAsyncExpression(expression, expectedValue) {
if (typeof expression !== "string")
debug("WARN: The expression arg in waitForExpression() should be a string.");
const evalExpression = `${expression} === ${expectedValue}`;
await waitFor(() => {
return eval(evalExpression);
});
debug(`PASS ${evalExpression}`);
}
async function waitForFocus(id) {
document.getElementById(id).focus();
let focusedElement;
await waitFor(() => {
focusedElement = accessibilityController.focusedElement;
return focusedElement && focusedElement.domIdentifier === id;
});
return focusedElement;
}
// Selects text within the element with the given ID, and returns the global selected `TextMarkerRange` after doing so.
// Optionally takes the AccessibilityUIElement associated with the web area as an argument. If not passed, we'll
// retrieve it in the function.
// NOTE: Only available for WK2.
async function selectElementTextById(id, axWebArea) {
const webArea = axWebArea ? axWebArea : accessibilityController.rootElement.childAtIndex(0);
const selection = window.getSelection();
selection.removeAllRanges();
await waitFor(() => {
const selectedRange = webArea.selectedTextMarkerRange();
return !webArea.isTextMarkerRangeValid(selectedRange);
});
const range = document.createRange();
range.selectNodeContents(document.getElementById(id));
selection.addRange(range);
let selectedRange;
await waitFor(() => {
selectedRange = webArea.selectedTextMarkerRange();
return webArea.isTextMarkerRangeValid(selectedRange);
});
return selectedRange;
}
// Selects a range of text delimited by the `startIndex` and `endIndex` within the element with the given ID,
// and returns the global selected `TextMarkerRange` after doing so. Optionally takes the AccessibilityUIElement
// associated with the web area as an argument. If not passed, we'll retrieve it in the function.
// NOTE: Only available for WK2.
async function selectPartialElementTextById(id, startIndex, endIndex, axWebArea) {
const webArea = axWebArea ? axWebArea : accessibilityController.rootElement.childAtIndex(0);
const selection = window.getSelection();
selection.removeAllRanges();
await waitFor(() => {
const selectedRange = webArea.selectedTextMarkerRange();
return !webArea.isTextMarkerRangeValid(selectedRange);
});
const element = document.getElementById(id);
if (element.setSelectionRange) {
// For textarea and input elements.
element.setSelectionRange(startIndex, endIndex);
} else {
// For contenteditable elements.
const range = document.createRange();
range.setStart(element.firstChild, startIndex);
range.setEnd(element.firstChild, endIndex);
selection.addRange(range);
}
let selectedRange;
await waitFor(() => {
selectedRange = webArea.selectedTextMarkerRange();
return webArea.isTextMarkerRangeValid(selectedRange);
});
return selectedRange;
}
function evalAndReturn(expression) {
if (typeof expression !== "string")
debug("FAIL: evalAndReturn() expects a string argument");
eval(expression);
return `${expression}\n`;
}
function logActiveElementAndSelectedChildren(axElement) {
output += ` activeElement: ${axElement.activeElement.domIdentifier}\n`;
var children = axElement.selectedChildren();
output += " selectedChildren: [ ";
for (let i = 0; i < children.length; ++i) {
if (i > 0)
output += ", ";
output += children[i].domIdentifier;
}
output += " ]\n";
}
function resetActiveElementAndSelectedChildren(id) {
Array.from(document.getElementById(id).children).forEach((child) => {
child.removeAttribute("aria-activedescendant");
child.removeAttribute("aria-selected");
});
}