1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
9 Log: "chrome://remote/content/shared/Log.sys.mjs",
10 waitForObserverTopic: "chrome://remote/content/marionette/sync.sys.mjs",
13 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
14 lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
17 ChromeUtils.defineLazyGetter(lazy, "service", () => {
19 return Cc["@mozilla.org/accessibilityService;1"].getService(
20 Ci.nsIAccessibilityService
23 lazy.logger.warn("Accessibility module is not present");
29 export const accessibility = {
36 * Accessible states used to check element"s state from the accessiblity API
39 * Note: if gecko is built with --disable-accessibility, the interfaces
40 * are not defined. This is why we use getters instead to be able to use
43 accessibility.State = {
45 return Ci.nsIAccessibleStates.STATE_UNAVAILABLE;
48 return Ci.nsIAccessibleStates.STATE_FOCUSABLE;
51 return Ci.nsIAccessibleStates.STATE_SELECTABLE;
54 return Ci.nsIAccessibleStates.STATE_SELECTED;
59 * Accessible object roles that support some action.
61 accessibility.ActionableRoles = new Set([
71 "listbox rich option",
86 * Factory function that constructs a new {@code accessibility.Checks}
87 * object with enforced strictness or not.
89 accessibility.get = function (strict = false) {
90 return new accessibility.Checks(!!strict);
94 * Wait for the document accessibility state to be different from STATE_BUSY.
96 * @param {Document} doc
97 * The document to wait for.
99 * A promise which resolves when the document's accessibility state is no
102 function waitForDocumentAccessibility(doc) {
103 const documentAccessible = accessibility.service.getAccessibleFor(doc);
105 documentAccessible.getState(state, {});
106 if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0) {
107 return Promise.resolve();
110 // Accessibility for the doc is busy, so wait for the state to change.
111 return lazy.waitForObserverTopic("accessible-event", {
112 checkFn: subject => {
113 // If event type does not match expected type, skip the event.
114 // If event's accessible does not match expected accessible,
116 const event = subject.QueryInterface(Ci.nsIAccessibleEvent);
118 event.eventType === Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE &&
119 event.accessible === documentAccessible
126 * Retrieve the Accessible for the provided element.
128 * @param {Element} element
129 * The element for which we need to retrieve the accessible.
131 * @returns {nsIAccessible|null}
132 * The Accessible object corresponding to the provided element or null if
133 * the accessibility service is not available.
135 accessibility.getAccessible = async function (element) {
136 if (!accessibility.service) {
140 // First, wait for accessibility to be ready for the element's document.
141 await waitForDocumentAccessibility(element.ownerDocument);
143 const acc = accessibility.service.getAccessibleFor(element);
148 // The Accessible doesn't exist yet. This can happen because a11y tree
149 // mutations happen during refresh driver ticks. Stop the refresh driver from
150 // doing its regular ticks and force two refresh driver ticks: the first to
151 // let layout update and notify a11y, and the second to let a11y process
153 const windowUtils = element.ownerGlobal.windowUtils;
154 windowUtils.advanceTimeAndRefresh(0);
155 windowUtils.advanceTimeAndRefresh(0);
156 // Go back to normal refresh driver ticks.
157 windowUtils.restoreNormalRefresh();
158 return accessibility.service.getAccessibleFor(element);
162 * Component responsible for interacting with platform accessibility
165 * Its methods serve as wrappers for testing content and chrome
166 * accessibility as well as accessibility of user interactions.
168 accessibility.Checks = class {
170 * @param {boolean} strict
171 * Flag indicating whether the accessibility issue should be logged
172 * or cause an error to be thrown. Default is to log to stdout.
174 constructor(strict) {
175 this.strict = strict;
179 * Assert that the element has a corresponding accessible object, and retrieve
180 * this accessible. Note that if the accessibility.Checks component was
181 * created in non-strict mode, this helper will not attempt to resolve the
182 * accessible at all and will simply return null.
184 * @param {DOMElement|XULElement} element
185 * Element to get the accessible object for.
186 * @param {boolean=} mustHaveAccessible
187 * Flag indicating that the element must have an accessible object.
188 * Defaults to not require this.
190 * @returns {Promise.<nsIAccessible>}
191 * Promise with an accessibility object for the given element.
193 async assertAccessible(element, mustHaveAccessible = false) {
198 const accessible = await accessibility.getAccessible(element);
199 if (!accessible && mustHaveAccessible) {
200 this.error("Element does not have an accessible object", element);
207 * Test if the accessible has a role that supports some arbitrary
210 * @param {nsIAccessible} accessible
214 * True if an actionable role is found on the accessible, false
217 isActionableRole(accessible) {
218 return accessibility.ActionableRoles.has(
219 accessibility.service.getStringRole(accessible.role)
224 * Test if an accessible has at least one action that it supports.
226 * @param {nsIAccessible} accessible
230 * True if the accessible has at least one supported action,
233 hasActionCount(accessible) {
234 return accessible.actionCount > 0;
238 * Test if an accessible has a valid name.
240 * @param {nsIAccessible} accessible
244 * True if the accessible has a non-empty valid name, or false if
245 * this is not the case.
247 hasValidName(accessible) {
248 return accessible.name && accessible.name.trim();
252 * Test if an accessible has a {@code hidden} attribute.
254 * @param {nsIAccessible} accessible
258 * True if the accessible object has a {@code hidden} attribute,
261 hasHiddenAttribute(accessible) {
264 hidden = accessible.attributes.getStringProperty("hidden");
266 // if the property is missing, error will be thrown
267 return hidden && hidden === "true";
271 * Verify if an accessible has a given state.
272 * Test if an accessible has a given state.
274 * @param {nsIAccessible} accessible
275 * Accessible object to test.
276 * @param {number} stateToMatch
280 * True if |accessible| has |stateToMatch|, false otherwise.
282 matchState(accessible, stateToMatch) {
284 accessible.getState(state, {});
285 return !!(state.value & stateToMatch);
289 * Test if an accessible is hidden from the user.
291 * @param {nsIAccessible} accessible
295 * True if element is hidden from user, false otherwise.
297 isHidden(accessible) {
303 if (this.hasHiddenAttribute(accessible)) {
306 accessible = accessible.parent;
312 * Test if the element's visible state corresponds to its accessibility
315 * @param {nsIAccessible} accessible
317 * @param {DOMElement|XULElement} element
318 * Element associated with |accessible|.
319 * @param {boolean} visible
320 * Visibility state of |element|.
322 * @throws ElementNotAccessibleError
323 * If |element|'s visibility state does not correspond to
326 assertVisible(accessible, element, visible) {
327 let hiddenAccessibility = this.isHidden(accessible);
330 if (visible && hiddenAccessibility) {
332 "Element is not currently visible via the accessibility API " +
333 "and may not be manipulated by it";
334 } else if (!visible && !hiddenAccessibility) {
336 "Element is currently only visible via the accessibility API " +
337 "and can be manipulated by it";
339 this.error(message, element);
343 * Test if the element's unavailable accessibility state matches the
346 * @param {nsIAccessible} accessible
348 * @param {DOMElement|XULElement} element
349 * Element associated with |accessible|.
350 * @param {boolean} enabled
351 * Enabled state of |element|.
353 * @throws ElementNotAccessibleError
354 * If |element|'s enabled state does not match |accessible|'s.
356 assertEnabled(accessible, element, enabled) {
361 let win = element.ownerGlobal;
362 let disabledAccessibility = this.matchState(
364 accessibility.State.Unavailable
367 win.getComputedStyle(element).getPropertyValue("pointer-events") !==
371 if (!explorable && !disabledAccessibility) {
373 "Element is enabled but is not explorable via the " +
375 } else if (enabled && disabledAccessibility) {
376 message = "Element is enabled but disabled via the accessibility API";
377 } else if (!enabled && !disabledAccessibility) {
378 message = "Element is disabled but enabled via the accessibility API";
380 this.error(message, element);
384 * Test if it is possible to activate an element with the accessibility
387 * @param {nsIAccessible} accessible
389 * @param {DOMElement|XULElement} element
390 * Element associated with |accessible|.
392 * @throws ElementNotAccessibleError
393 * If it is impossible to activate |element| with |accessible|.
395 assertActionable(accessible, element) {
401 if (!this.hasActionCount(accessible)) {
402 message = "Element does not support any accessible actions";
403 } else if (!this.isActionableRole(accessible)) {
405 "Element does not have a correct accessibility role " +
406 "and may not be manipulated via the accessibility API";
407 } else if (!this.hasValidName(accessible)) {
408 message = "Element is missing an accessible name";
409 } else if (!this.matchState(accessible, accessibility.State.Focusable)) {
410 message = "Element is not focusable via the accessibility API";
413 this.error(message, element);
417 * Test that an element's selected state corresponds to its
418 * accessibility API selected state.
420 * @param {nsIAccessible} accessible
422 * @param {DOMElement|XULElement} element
423 * Element associated with |accessible|.
424 * @param {boolean} selected
425 * The |element|s selected state.
427 * @throws ElementNotAccessibleError
428 * If |element|'s selected state does not correspond to
431 assertSelected(accessible, element, selected) {
436 // element is not selectable via the accessibility API
437 if (!this.matchState(accessible, accessibility.State.Selectable)) {
441 let selectedAccessibility = this.matchState(
443 accessibility.State.Selected
447 if (selected && !selectedAccessibility) {
449 "Element is selected but not selected via the accessibility API";
450 } else if (!selected && selectedAccessibility) {
452 "Element is not selected but selected via the accessibility API";
454 this.error(message, element);
458 * Throw an error if strict accessibility checks are enforced and log
459 * the error to the log.
461 * @param {string} message
462 * @param {DOMElement|XULElement} element
463 * Element that caused an error.
465 * @throws ElementNotAccessibleError
466 * If |strict| is true.
468 error(message, element) {
469 if (!message || !this.strict) {
473 let { id, tagName, className } = element;
474 message += `: id: ${id}, tagName: ${tagName}, className: ${className}`;
477 throw new lazy.error.ElementNotAccessibleError(message);