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 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
8 const { XPCOMUtils } = ChromeUtils.import(
9 "resource://gre/modules/XPCOMUtils.jsm"
12 const { ElementNotAccessibleError } = ChromeUtils.import(
13 "chrome://marionette/content/error.js"
15 const { Log } = ChromeUtils.import("chrome://marionette/content/log.js");
17 XPCOMUtils.defineLazyGetter(this, "logger", Log.get);
19 XPCOMUtils.defineLazyGetter(this, "service", () => {
21 return Cc["@mozilla.org/accessibilityService;1"].getService(
22 Ci.nsIAccessibilityService
25 logger.warn("Accessibility module is not present");
30 this.EXPORTED_SYMBOLS = ["accessibility"];
33 this.accessibility = {
40 * Accessible states used to check element"s state from the accessiblity API
43 * Note: if gecko is built with --disable-accessibility, the interfaces
44 * are not defined. This is why we use getters instead to be able to use
47 accessibility.State = {
49 return Ci.nsIAccessibleStates.STATE_UNAVAILABLE;
52 return Ci.nsIAccessibleStates.STATE_FOCUSABLE;
55 return Ci.nsIAccessibleStates.STATE_SELECTABLE;
58 return Ci.nsIAccessibleStates.STATE_SELECTED;
63 * Accessible object roles that support some action.
65 accessibility.ActionableRoles = new Set([
75 "listbox rich option",
90 * Factory function that constructs a new {@code accessibility.Checks}
91 * object with enforced strictness or not.
93 accessibility.get = function(strict = false) {
94 return new accessibility.Checks(!!strict);
98 * Component responsible for interacting with platform accessibility
101 * Its methods serve as wrappers for testing content and chrome
102 * accessibility as well as accessibility of user interactions.
104 accessibility.Checks = class {
106 * @param {boolean} strict
107 * Flag indicating whether the accessibility issue should be logged
108 * or cause an error to be thrown. Default is to log to stdout.
110 constructor(strict) {
111 this.strict = strict;
115 * Get an accessible object for an element.
117 * @param {DOMElement|XULElement} element
118 * Element to get the accessible object for.
119 * @param {boolean=} mustHaveAccessible
120 * Flag indicating that the element must have an accessible object.
121 * Defaults to not require this.
123 * @return {Promise.<nsIAccessible>}
124 * Promise with an accessibility object for the given element.
126 getAccessible(element, mustHaveAccessible = false) {
128 return Promise.resolve();
131 return new Promise((resolve, reject) => {
132 if (!accessibility.service) {
137 // First, check if accessibility is ready.
138 let docAcc = accessibility.service.getAccessibleFor(
139 element.ownerDocument
142 docAcc.getState(state, {});
143 if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0) {
144 // Accessibility is ready, resolve immediately.
145 let acc = accessibility.service.getAccessibleFor(element);
146 if (mustHaveAccessible && !acc) {
153 // Accessibility for the doc is busy, so wait for the state to change.
154 let eventObserver = {
155 observe(subject, topic) {
156 if (topic !== "accessible-event") {
160 // If event type does not match expected type, skip the event.
161 let event = subject.QueryInterface(Ci.nsIAccessibleEvent);
162 if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) {
166 // If event's accessible does not match expected accessible,
168 if (event.accessible !== docAcc) {
172 Services.obs.removeObserver(this, "accessible-event");
173 let acc = accessibility.service.getAccessibleFor(element);
174 if (mustHaveAccessible && !acc) {
181 Services.obs.addObserver(eventObserver, "accessible-event");
183 this.error("Element does not have an accessible object", element)
188 * Test if the accessible has a role that supports some arbitrary
191 * @param {nsIAccessible} accessible
195 * True if an actionable role is found on the accessible, false
198 isActionableRole(accessible) {
199 return accessibility.ActionableRoles.has(
200 accessibility.service.getStringRole(accessible.role)
205 * Test if an accessible has at least one action that it supports.
207 * @param {nsIAccessible} accessible
211 * True if the accessible has at least one supported action,
214 hasActionCount(accessible) {
215 return accessible.actionCount > 0;
219 * Test if an accessible has a valid name.
221 * @param {nsIAccessible} accessible
225 * True if the accessible has a non-empty valid name, or false if
226 * this is not the case.
228 hasValidName(accessible) {
229 return accessible.name && accessible.name.trim();
233 * Test if an accessible has a {@code hidden} attribute.
235 * @param {nsIAccessible} accessible
239 * True if the accessible object has a {@code hidden} attribute,
242 hasHiddenAttribute(accessible) {
245 hidden = accessible.attributes.getStringProperty("hidden");
247 // if the property is missing, error will be thrown
248 return hidden && hidden === "true";
252 * Verify if an accessible has a given state.
253 * Test if an accessible has a given state.
255 * @param {nsIAccessible} accessible
256 * Accessible object to test.
257 * @param {number} stateToMatch
261 * True if |accessible| has |stateToMatch|, false otherwise.
263 matchState(accessible, stateToMatch) {
265 accessible.getState(state, {});
266 return !!(state.value & stateToMatch);
270 * Test if an accessible is hidden from the user.
272 * @param {nsIAccessible} accessible
276 * True if element is hidden from user, false otherwise.
278 isHidden(accessible) {
284 if (this.hasHiddenAttribute(accessible)) {
287 accessible = accessible.parent;
293 * Test if the element's visible state corresponds to its accessibility
296 * @param {nsIAccessible} accessible
298 * @param {DOMElement|XULElement} element
299 * Element associated with |accessible|.
300 * @param {boolean} visible
301 * Visibility state of |element|.
303 * @throws ElementNotAccessibleError
304 * If |element|'s visibility state does not correspond to
307 assertVisible(accessible, element, visible) {
308 let hiddenAccessibility = this.isHidden(accessible);
311 if (visible && hiddenAccessibility) {
313 "Element is not currently visible via the accessibility API " +
314 "and may not be manipulated by it";
315 } else if (!visible && !hiddenAccessibility) {
317 "Element is currently only visible via the accessibility API " +
318 "and can be manipulated by it";
320 this.error(message, element);
324 * Test if the element's unavailable accessibility state matches the
327 * @param {nsIAccessible} accessible
329 * @param {DOMElement|XULElement} element
330 * Element associated with |accessible|.
331 * @param {boolean} enabled
332 * Enabled state of |element|.
334 * @throws ElementNotAccessibleError
335 * If |element|'s enabled state does not match |accessible|'s.
337 assertEnabled(accessible, element, enabled) {
342 let win = element.ownerGlobal;
343 let disabledAccessibility = this.matchState(
345 accessibility.State.Unavailable
348 win.getComputedStyle(element).getPropertyValue("pointer-events") !==
352 if (!explorable && !disabledAccessibility) {
354 "Element is enabled but is not explorable via the " +
356 } else if (enabled && disabledAccessibility) {
357 message = "Element is enabled but disabled via the accessibility API";
358 } else if (!enabled && !disabledAccessibility) {
359 message = "Element is disabled but enabled via the accessibility API";
361 this.error(message, element);
365 * Test if it is possible to activate an element with the accessibility
368 * @param {nsIAccessible} accessible
370 * @param {DOMElement|XULElement} element
371 * Element associated with |accessible|.
373 * @throws ElementNotAccessibleError
374 * If it is impossible to activate |element| with |accessible|.
376 assertActionable(accessible, element) {
382 if (!this.hasActionCount(accessible)) {
383 message = "Element does not support any accessible actions";
384 } else if (!this.isActionableRole(accessible)) {
386 "Element does not have a correct accessibility role " +
387 "and may not be manipulated via the accessibility API";
388 } else if (!this.hasValidName(accessible)) {
389 message = "Element is missing an accessible name";
390 } else if (!this.matchState(accessible, accessibility.State.Focusable)) {
391 message = "Element is not focusable via the accessibility API";
394 this.error(message, element);
398 * Test that an element's selected state corresponds to its
399 * accessibility API selected state.
401 * @param {nsIAccessible} accessible
403 * @param {DOMElement|XULElement}
404 * Element associated with |accessible|.
405 * @param {boolean} selected
406 * The |element|s selected state.
408 * @throws ElementNotAccessibleError
409 * If |element|'s selected state does not correspond to
412 assertSelected(accessible, element, selected) {
417 // element is not selectable via the accessibility API
418 if (!this.matchState(accessible, accessibility.State.Selectable)) {
422 let selectedAccessibility = this.matchState(
424 accessibility.State.Selected
428 if (selected && !selectedAccessibility) {
430 "Element is selected but not selected via the accessibility API";
431 } else if (!selected && selectedAccessibility) {
433 "Element is not selected but selected via the accessibility API";
435 this.error(message, element);
439 * Throw an error if strict accessibility checks are enforced and log
440 * the error to the log.
442 * @param {string} message
443 * @param {DOMElement|XULElement} element
444 * Element that caused an error.
446 * @throws ElementNotAccessibleError
447 * If |strict| is true.
449 error(message, element) {
450 if (!message || !this.strict) {
454 let { id, tagName, className } = element;
455 message += `: id: ${id}, tagName: ${tagName}, className: ${className}`;
458 throw new ElementNotAccessibleError(message);