123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
- "use strict";
- const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
- Cu.import("resource://gre/modules/XPCOMUtils.jsm");
- Cu.import("resource://gre/modules/Log.jsm");
- const logger = Log.repository.getLogger("Marionette");
- Cu.import("chrome://marionette/content/error.js");
- XPCOMUtils.defineLazyModuleGetter(
- this, "setInterval", "resource://gre/modules/Timer.jsm");
- XPCOMUtils.defineLazyModuleGetter(
- this, "clearInterval", "resource://gre/modules/Timer.jsm");
- XPCOMUtils.defineLazyGetter(this, "service", () => {
- let service;
- try {
- service = Cc["@mozilla.org/accessibilityService;1"].getService(
- Ci.nsIAccessibilityService);
- } catch (e) {
- logger.warn("Accessibility module is not present");
- } finally {
- return service;
- }
- });
- this.EXPORTED_SYMBOLS = ["accessibility"];
- /**
- * Number of attempts to get an accessible object for an element.
- * We attempt more than once because accessible tree can be out of sync
- * with the DOM tree for a short period of time.
- */
- const GET_ACCESSIBLE_ATTEMPTS = 100;
- /**
- * An interval between attempts to retrieve an accessible object for an
- * element.
- */
- const GET_ACCESSIBLE_ATTEMPT_INTERVAL = 10;
- this.accessibility = {
- get service() {
- return service;
- }
- };
- /**
- * Accessible states used to check element"s state from the accessiblity API
- * perspective.
- * Note: if gecko is built with --disable-accessibility, the interfaces are not
- * defined. This is why we use getters instead to be able to use these
- * statically.
- */
- accessibility.State = {
- get Unavailable() {
- return Ci.nsIAccessibleStates.STATE_UNAVAILABLE;
- },
- get Focusable() {
- return Ci.nsIAccessibleStates.STATE_FOCUSABLE;
- },
- get Selectable() {
- return Ci.nsIAccessibleStates.STATE_SELECTABLE;
- },
- get Selected() {
- return Ci.nsIAccessibleStates.STATE_SELECTED;
- }
- };
- /**
- * Accessible object roles that support some action.
- */
- accessibility.ActionableRoles = new Set([
- "checkbutton",
- "check menu item",
- "check rich option",
- "combobox",
- "combobox option",
- "entry",
- "key",
- "link",
- "listbox option",
- "listbox rich option",
- "menuitem",
- "option",
- "outlineitem",
- "pagetab",
- "pushbutton",
- "radiobutton",
- "radio menu item",
- "rowheader",
- "slider",
- "spinbutton",
- "switch",
- ]);
- /**
- * Factory function that constructs a new {@code accessibility.Checks}
- * object with enforced strictness or not.
- */
- accessibility.get = function (strict = false) {
- return new accessibility.Checks(!!strict);
- };
- /**
- * Component responsible for interacting with platform accessibility
- * API.
- *
- * Its methods serve as wrappers for testing content and chrome
- * accessibility as well as accessibility of user interactions.
- */
- accessibility.Checks = class {
- /**
- * @param {boolean} strict
- * Flag indicating whether the accessibility issue should be logged
- * or cause an error to be thrown. Default is to log to stdout.
- */
- constructor(strict) {
- this.strict = strict;
- }
- /**
- * Get an accessible object for an element.
- *
- * @param {DOMElement|XULElement} element
- * Element to get the accessible object for.
- * @param {boolean=} mustHaveAccessible
- * Flag indicating that the element must have an accessible object.
- * Defaults to not require this.
- *
- * @return {Promise: nsIAccessible}
- * Promise with an accessibility object for the given element.
- */
- getAccessible(element, mustHaveAccessible = false) {
- if (!this.strict) {
- return Promise.resolve();
- }
- return new Promise((resolve, reject) => {
- if (!accessibility.service) {
- reject();
- return;
- }
- let acc = accessibility.service.getAccessibleFor(element);
- if (acc || !mustHaveAccessible) {
- // if accessible object is found, return it;
- // if it is not required, also resolve
- resolve(acc);
- } else {
- // if we require an accessible object, we need to poll for it
- // because accessible tree might be
- // out of sync with DOM tree for a short time
- let attempts = GET_ACCESSIBLE_ATTEMPTS;
- let intervalId = setInterval(() => {
- let acc = accessibility.service.getAccessibleFor(element);
- if (acc || --attempts <= 0) {
- clearInterval(intervalId);
- if (acc) {
- resolve(acc);
- } else {
- reject();
- }
- }
- }, GET_ACCESSIBLE_ATTEMPT_INTERVAL);
- }
- }).catch(() => this.error(
- "Element does not have an accessible object", element));
- };
- /**
- * Test if the accessible has a role that supports some arbitrary
- * action.
- *
- * @param {nsIAccessible} accessible
- * Accessible object.
- *
- * @return {boolean}
- * True if an actionable role is found on the accessible, false
- * otherwise.
- */
- isActionableRole(accessible) {
- return accessibility.ActionableRoles.has(
- accessibility.service.getStringRole(accessible.role));
- }
- /**
- * Test if an accessible has at least one action that it supports.
- *
- * @param {nsIAccessible} accessible
- * Accessible object.
- *
- * @return {boolean}
- * True if the accessible has at least one supported action,
- * false otherwise.
- */
- hasActionCount(accessible) {
- return accessible.actionCount > 0;
- }
- /**
- * Test if an accessible has a valid name.
- *
- * @param {nsIAccessible} accessible
- * Accessible object.
- *
- * @return {boolean}
- * True if the accessible has a non-empty valid name, or false if
- * this is not the case.
- */
- hasValidName(accessible) {
- return accessible.name && accessible.name.trim();
- }
- /**
- * Test if an accessible has a {@code hidden} attribute.
- *
- * @param {nsIAccessible} accessible
- * Accessible object.
- *
- * @return {boolean}
- * True if the accesible object has a {@code hidden} attribute,
- * false otherwise.
- */
- hasHiddenAttribute(accessible) {
- let hidden = false;
- try {
- hidden = accessible.attributes.getStringProperty("hidden");
- } finally {
- // if the property is missing, error will be thrown
- return hidden && hidden === "true";
- }
- }
- /**
- * Verify if an accessible has a given state.
- * Test if an accessible has a given state.
- *
- * @param {nsIAccessible} accessible
- * Accessible object to test.
- * @param {number} stateToMatch
- * State to match.
- *
- * @return {boolean}
- * True if |accessible| has |stateToMatch|, false otherwise.
- */
- matchState(accessible, stateToMatch) {
- let state = {};
- accessible.getState(state, {});
- return !!(state.value & stateToMatch);
- }
- /**
- * Test if an accessible is hidden from the user.
- *
- * @param {nsIAccessible} accessible
- * Accessible object.
- *
- * @return {boolean}
- * True if element is hidden from user, false otherwise.
- */
- isHidden(accessible) {
- while (accessible) {
- if (this.hasHiddenAttribute(accessible)) {
- return true;
- }
- accessible = accessible.parent;
- }
- return false;
- }
- /**
- * Test if the element's visible state corresponds to its accessibility
- * API visibility.
- *
- * @param {nsIAccessible} accessible
- * Accessible object.
- * @param {DOMElement|XULElement} element
- * Element associated with |accessible|.
- * @param {boolean} visible
- * Visibility state of |element|.
- *
- * @throws ElementNotAccessibleError
- * If |element|'s visibility state does not correspond to
- * |accessible|'s.
- */
- assertVisible(accessible, element, visible) {
- if (!accessible) {
- return;
- }
- let hiddenAccessibility = this.isHidden(accessible);
- let message;
- if (visible && hiddenAccessibility) {
- message = "Element is not currently visible via the accessibility API " +
- "and may not be manipulated by it";
- } else if (!visible && !hiddenAccessibility) {
- message = "Element is currently only visible via the accessibility API " +
- "and can be manipulated by it";
- }
- this.error(message, element);
- }
- /**
- * Test if the element's unavailable accessibility state matches the
- * enabled state.
- *
- * @param {nsIAccessible} accessible
- * Accessible object.
- * @param {DOMElement|XULElement} element
- * Element associated with |accessible|.
- * @param {boolean} enabled
- * Enabled state of |element|.
- *
- * @throws ElementNotAccessibleError
- * If |element|'s enabled state does not match |accessible|'s.
- */
- assertEnabled(accessible, element, enabled) {
- if (!accessible) {
- return;
- }
- let win = element.ownerDocument.defaultView;
- let disabledAccessibility = this.matchState(
- accessible, accessibility.State.Unavailable);
- let explorable = win.getComputedStyle(element)
- .getPropertyValue("pointer-events") !== "none";
- let message;
- if (!explorable && !disabledAccessibility) {
- message = "Element is enabled but is not explorable via the " +
- "accessibility API";
- } else if (enabled && disabledAccessibility) {
- message = "Element is enabled but disabled via the accessibility API";
- } else if (!enabled && !disabledAccessibility) {
- message = "Element is disabled but enabled via the accessibility API";
- }
- this.error(message, element);
- }
- /**
- * Test if it is possible to activate an element with the accessibility
- * API.
- *
- * @param {nsIAccessible} accessible
- * Accessible object.
- * @param {DOMElement|XULElement} element
- * Element associated with |accessible|.
- *
- * @throws ElementNotAccessibleError
- * If it is impossible to activate |element| with |accessible|.
- */
- assertActionable(accessible, element) {
- if (!accessible) {
- return;
- }
- let message;
- if (!this.hasActionCount(accessible)) {
- message = "Element does not support any accessible actions";
- } else if (!this.isActionableRole(accessible)) {
- message = "Element does not have a correct accessibility role " +
- "and may not be manipulated via the accessibility API";
- } else if (!this.hasValidName(accessible)) {
- message = "Element is missing an accessible name";
- } else if (!this.matchState(accessible, accessibility.State.Focusable)) {
- message = "Element is not focusable via the accessibility API";
- }
- this.error(message, element);
- }
- /**
- * Test that an element's selected state corresponds to its
- * accessibility API selected state.
- *
- * @param {nsIAccessible} accessible
- * Accessible object.
- * @param {DOMElement|XULElement}
- * Element associated with |accessible|.
- * @param {boolean} selected
- * The |element|s selected state.
- *
- * @throws ElementNotAccessibleError
- * If |element|'s selected state does not correspond to
- * |accessible|'s.
- */
- assertSelected(accessible, element, selected) {
- if (!accessible) {
- return;
- }
- // element is not selectable via the accessibility API
- if (!this.matchState(accessible, accessibility.State.Selectable)) {
- return;
- }
- let selectedAccessibility = this.matchState(accessible, accessibility.State.Selected);
- let message;
- if (selected && !selectedAccessibility) {
- message = "Element is selected but not selected via the accessibility API";
- } else if (!selected && selectedAccessibility) {
- message = "Element is not selected but selected via the accessibility API";
- }
- this.error(message, element);
- }
- /**
- * Throw an error if strict accessibility checks are enforced and log
- * the error to the log.
- *
- * @param {string} message
- * @param {DOMElement|XULElement} element
- * Element that caused an error.
- *
- * @throws ElementNotAccessibleError
- * If |strict| is true.
- */
- error(message, element) {
- if (!message || !this.strict) {
- return;
- }
- if (element) {
- let {id, tagName, className} = element;
- message += `: id: ${id}, tagName: ${tagName}, className: ${className}`;
- }
- throw new ElementNotAccessibleError(message);
- }
- };
|