diff --git a/testing/marionette/accessibility.js b/testing/marionette/accessibility.js index 7f3aeded98d8..96bda0bd53bb 100644 --- a/testing/marionette/accessibility.js +++ b/testing/marionette/accessibility.js @@ -2,244 +2,209 @@ * 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"; +/* global Accessibility, Components, Log, ElementNotAccessibleError, + XPCOMUtils */ -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Log.jsm'); -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.defineLazyModuleGetter(this, 'ElementNotAccessibleError', + '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, "retrieval", - () => Cc["@mozilla.org/accessibleRetrieval;1"].getService(Ci.nsIAccessibleRetrieval)); - -this.EXPORTED_SYMBOLS = ["accessibility"]; - -const logger = Log.repository.getLogger("Marionette"); +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 = {}; - -/** - * Accessible states used to check element"s state from the accessiblity API + * Accessible states used to check element's state from the accessiblity API * perspective. */ -accessibility.State = { - Unavailable: Ci.nsIAccessibleStates.STATE_UNAVAILABLE, - Focusable: Ci.nsIAccessibleStates.STATE_FOCUSABLE, - Selectable: Ci.nsIAccessibleStates.STATE_SELECTABLE, - Selected: Ci.nsIAccessibleStates.STATE_SELECTED, +const states = { + unavailable: Ci.nsIAccessibleStates.STATE_UNAVAILABLE, + focusable: Ci.nsIAccessibleStates.STATE_FOCUSABLE, + selectable: Ci.nsIAccessibleStates.STATE_SELECTABLE, + selected: 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", -]); - +var logger = Log.repository.getLogger('Marionette'); /** - * 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. + * 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. * - * Its methods serve as wrappers for testing content and chrome - * accessibility as well as accessibility of user interactions. + * @param {Function} getCapabilies + * Session capabilities getter. */ -accessibility.Checks = class { +this.Accessibility = function Accessibility(getCapabilies = () => {}) { + // A flag indicating whether the accessibility issue should be logged or cause + // an exception. Default: log to stdout. + Object.defineProperty(this, 'strict', { + configurable: true, + get: function() { + let capabilies = getCapabilies(); + return !!capabilies.raisesAccessibilityExceptions; + } + }); + // An interface for in-process accessibility clients + // Note: we access it lazily to not enable accessibility when it is not needed + Object.defineProperty(this, 'retrieval', { + configurable: true, + get: function() { + delete this.retrieval; + this.retrieval = Cc[ + '@mozilla.org/accessibleRetrieval;1'].getService( + Ci.nsIAccessibleRetrieval); + return this.retrieval; + } + }); +}; + +Accessibility.prototype = { /** - * @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. + * 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. + * @type {Number} */ - constructor(strict) { - this.strict = strict; - } + GET_ACCESSIBLE_ATTEMPTS: 100, /** - * 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 {nsIAccessible} - * Accessibility object for the given element. + * An interval between attempts to retrieve an accessible object for an + * element. + * @type {Number} ms */ - getAccessible(element, mustHaveAccessible = false) { + GET_ACCESSIBLE_ATTEMPT_INTERVAL: 10, + + /** + * Accessible object roles that support some action + * @type Object + */ + ACTIONABLE_ROLES: new Set([ + 'pushbutton', + 'checkbutton', + 'combobox', + 'key', + 'link', + 'menuitem', + 'check menu item', + 'radio menu item', + 'option', + 'listbox option', + 'listbox rich option', + 'check rich option', + 'combobox option', + 'radiobutton', + 'rowheader', + 'switch', + 'slider', + 'spinbutton', + 'pagetab', + 'entry', + 'outlineitem' + ]), + + /** + * Get an accessible object for a DOM element + * @param nsIDOMElement element + * @param Boolean mustHaveAccessible a flag indicating that the element must + * have an accessible object + * @return nsIAccessible object for the element + */ + getAccessibleObject(element, mustHaveAccessible = false) { return new Promise((resolve, reject) => { - let acc = retrieval.getAccessibleFor(element); + let acc = this.retrieval.getAccessibleFor(element); - // if accessible object is found, return it; - // if it is not required, also resolve if (acc || !mustHaveAccessible) { + // If accessible object is found, return it. If it is not required, + // also resolve. resolve(acc); - - // if we must have an accessible but are strict, - // reject now and avoid polling for an accessible object } else if (mustHaveAccessible && !this.strict) { + // If we must have an accessible but are not raising accessibility + // exceptions, reject now and avoid polling for an accessible object. reject(); - - // 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 } else { - let attempts = GET_ACCESSIBLE_ATTEMPTS; + // 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 = this.GET_ACCESSIBLE_ATTEMPTS; let intervalId = setInterval(() => { - let acc = retrieval.getAccessibleFor(element); + let acc = this.retrieval.getAccessibleFor(element); if (acc || --attempts <= 0) { clearInterval(intervalId); - if (acc) { - resolve(acc); - } else { - reject(); - } + if (acc) { resolve(acc); } + else { reject(); } } - }, GET_ACCESSIBLE_ATTEMPT_INTERVAL); + }, this.GET_ACCESSIBLE_ATTEMPT_INTERVAL); } }).catch(() => this.error( - "Element does not have an accessible object", element)); - }; + '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. + * Check if the accessible has a role that supports some action + * @param nsIAccessible object + * @return Boolean an indicator of role being actionable */ isActionableRole(accessible) { - return accessibility.ActionableRoles.has( - retrieval.getStringRole(accessible.role)); - } + return this.ACTIONABLE_ROLES.has( + this.retrieval.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. + * Determine if an accessible has at least one action that it supports + * @param nsIAccessible object + * @return Boolean an indicator of supporting at least one accessible action */ 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. + * Determine if an accessible has a valid name + * @param nsIAccessible object + * @return Boolean an indicator that the element has a non empty valid name */ 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. + * Check if an accessible has a set hidden attribute + * @param nsIAccessible object + * @return Boolean an indicator that the element has a hidden accessible + * attribute set to true */ hasHiddenAttribute(accessible) { let hidden = false; try { - hidden = accessible.attributes.getStringProperty("hidden"); + hidden = accessible.attributes.getStringProperty('hidden'); } finally { - // if the property is missing, error will be thrown - return hidden && hidden === "true"; + // If the property is missing, exception 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. + * Verify if an accessible has a given state + * @param nsIAccessible object + * @param Number stateToMatch the state to match + * @return Boolean accessible has a state */ 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. + * Check if an accessible is hidden from the user of the accessibility API + * @param nsIAccessible object + * @return Boolean an indicator that the element is hidden from the user */ isHidden(accessible) { while (accessible) { @@ -249,156 +214,12 @@ accessibility.Checks = class { 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. - */ - checkVisible(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. - */ - checkEnabled(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|. - */ - checkActionable(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. - */ - checkSelected(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. + * Send an error message or log the error message in the log + * @param String message + * @param DOMElement element that caused an error */ error(message, element) { if (!message) { @@ -412,6 +233,107 @@ accessibility.Checks = class { throw new ElementNotAccessibleError(message); } logger.debug(message); - } + }, + /** + * Check if the element's visible state corresponds to its accessibility API + * visibility + * @param nsIAccessible object + * @param WebElement corresponding to nsIAccessible object + * @param Boolean visible element's visibility state + */ + checkVisible(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); + }, + + /** + * Check if the element's unavailable accessibility state matches the enabled + * state + * @param nsIAccessible object + * @param WebElement corresponding to nsIAccessible object + * @param Boolean enabled element's enabled state + * @param Object container frame and optional ShadowDOM + */ + checkEnabled(accessible, element, enabled, container) { + if (!accessible) { + return; + } + let disabledAccessibility = this.matchState(accessible, states.unavailable); + let explorable = container.frame.document.defaultView.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); + }, + + /** + * Check if it is possible to activate an element with the accessibility API + * @param nsIAccessible object + * @param WebElement corresponding to nsIAccessible object + */ + checkActionable(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, states.focusable)) { + message = 'Element is not focusable via the accessibility API'; + } + this.error(message, element); + }, + + /** + * Check if element's selected state corresponds to its accessibility API + * selected state. + * @param nsIAccessible object + * @param WebElement corresponding to nsIAccessible object + * @param Boolean selected element's selected state + */ + checkSelected(accessible, element, selected) { + if (!accessible) { + return; + } + if (!this.matchState(accessible, states.selectable)) { + // Element is not selectable via the accessibility API + return; + } + + let selectedAccessibility = this.matchState(accessible, states.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); + } }; diff --git a/testing/marionette/driver.js b/testing/marionette/driver.js index 5a0d3875b918..74c1fca1ffc4 100644 --- a/testing/marionette/driver.js +++ b/testing/marionette/driver.js @@ -158,6 +158,8 @@ this.GeckoDriver = function(appName, device, stopSignal, emulator) { "version": Services.appinfo.version, }; + this.interactions = new Interactions(() => this.sessionCapabilities); + this.mm = globalMessageManager; this.listener = proxy.toListener(() => this.mm, this.sendAsync.bind(this)); @@ -1964,9 +1966,8 @@ GeckoDriver.prototype.clickElement = function*(cmd, resp) { switch (this.context) { case Context.CHROME: let win = this.getCurrentWindow(); - let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win}); - yield interaction.clickElement( - el, this.sessionCapabilities.raisesAccessibilityExceptions); + yield this.interactions.clickElement({ frame: win }, + this.curBrowser.elementManager, id); break; case Context.CONTENT: @@ -2064,10 +2065,8 @@ GeckoDriver.prototype.isElementDisplayed = function*(cmd, resp) { switch (this.context) { case Context.CHROME: let win = this.getCurrentWindow(); - let el = this.curBrowser.elementManager.getKnownElement( - id, {frame: win}); - resp.body.value = yield interaction.isElementDisplayed( - el, this.sessionCapabilities.raisesAccessibilityExceptions); + resp.body.value = yield this.interactions.isElementDisplayed( + {frame: win}, this.curBrowser.elementManager, id); break; case Context.CONTENT: @@ -2114,10 +2113,8 @@ GeckoDriver.prototype.isElementEnabled = function*(cmd, resp) { case Context.CHROME: // Selenium atom doesn't quite work here let win = this.getCurrentWindow(); - let el = this.curBrowser.elementManager.getKnownElement( - id, {frame: win}); - resp.body.value = yield interaction.isElementEnabled( - el, this.sessionCapabilities.raisesAccessibilityExceptions); + resp.body.value = yield this.interactions.isElementEnabled( + {frame: win}, this.curBrowser.elementManager, id); break; case Context.CONTENT: @@ -2139,10 +2136,8 @@ GeckoDriver.prototype.isElementSelected = function*(cmd, resp) { case Context.CHROME: // Selenium atom doesn't quite work here let win = this.getCurrentWindow(); - let el = this.curBrowser.elementManager.getKnownElement( - id, {frame: win}); - resp.body.value = yield interaction.isElementSelected( - el, this.sessionCapabilities.raisesAccessibilityExceptions); + resp.body.value = yield this.interactions.isElementSelected( + { frame: win }, this.curBrowser.elementManager, id); break; case Context.CONTENT: @@ -2191,10 +2186,8 @@ GeckoDriver.prototype.sendKeysToElement = function*(cmd, resp) { switch (this.context) { case Context.CHROME: let win = this.getCurrentWindow(); - let el = this.curBrowser.elementManager.getKnownElement( - id, {frame: win}); - yield interaction.sendKeysToElement( - el, value, true, this.sessionCapabilities.raisesAccessibilityExceptions); + yield this.interactions.sendKeysToElement( + { frame: win }, this.curBrowser.elementManager, id, value, true); break; case Context.CONTENT: diff --git a/testing/marionette/interaction.js b/testing/marionette/interaction.js index 81cb3cef8da0..7b916e3c2f96 100644 --- a/testing/marionette/interaction.js +++ b/testing/marionette/interaction.js @@ -12,222 +12,230 @@ Cu.import("chrome://marionette/content/error.js"); Cu.import("chrome://marionette/content/element.js"); Cu.import("chrome://marionette/content/event.js"); -this.EXPORTED_SYMBOLS = ["interaction"]; +this.EXPORTED_SYMBOLS = ["Interactions"]; /** - * XUL elements that support disabled attribute. + * XUL elements that support disabled attribtue. */ const DISABLED_ATTRIBUTE_SUPPORTED_XUL = new Set([ - "ARROWSCROLLBOX", - "BUTTON", - "CHECKBOX", - "COLORPICKER", - "COMMAND", - "DATEPICKER", - "DESCRIPTION", - "KEY", - "KEYSET", - "LABEL", - "LISTBOX", - "LISTCELL", - "LISTHEAD", - "LISTHEADER", - "LISTITEM", - "MENU", - "MENUITEM", - "MENULIST", - "MENUSEPARATOR", - "PREFERENCE", - "RADIO", - "RADIOGROUP", - "RICHLISTBOX", - "RICHLISTITEM", - "SCALE", - "TAB", - "TABS", - "TEXTBOX", - "TIMEPICKER", - "TOOLBARBUTTON", - "TREE", + 'ARROWSCROLLBOX', + 'BUTTON', + 'CHECKBOX', + 'COLORPICKER', + 'COMMAND', + 'DATEPICKER', + 'DESCRIPTION', + 'KEY', + 'KEYSET', + 'LABEL', + 'LISTBOX', + 'LISTCELL', + 'LISTHEAD', + 'LISTHEADER', + 'LISTITEM', + 'MENU', + 'MENUITEM', + 'MENULIST', + 'MENUSEPARATOR', + 'PREFERENCE', + 'RADIO', + 'RADIOGROUP', + 'RICHLISTBOX', + 'RICHLISTITEM', + 'SCALE', + 'TAB', + 'TABS', + 'TEXTBOX', + 'TIMEPICKER', + 'TOOLBARBUTTON', + 'TREE' ]); /** * XUL elements that support checked property. */ const CHECKED_PROPERTY_SUPPORTED_XUL = new Set([ - "BUTTON", - "CHECKBOX", - "LISTITEM", - "TOOLBARBUTTON", + 'BUTTON', + 'CHECKBOX', + 'LISTITEM', + 'TOOLBARBUTTON' ]); /** * XUL elements that support selected property. */ const SELECTED_PROPERTY_SUPPORTED_XUL = new Set([ - "LISTITEM", - "MENU", - "MENUITEM", - "MENUSEPARATOR", - "RADIO", - "RICHLISTITEM", - "TAB", + 'LISTITEM', + 'MENU', + 'MENUITEM', + 'MENUSEPARATOR', + 'RADIO', + 'RICHLISTITEM', + 'TAB' ]); -this.interaction = {}; - /** - * Interact with an element by clicking it. - * - * @param {DOMElement|XULElement} el - * Element to click. - * @param {boolean=} strict - * Enforce strict accessibility tests. + * A collection of interactions available in marionette. + * @type {Object} */ -interaction.clickElement = function(el, strict = false) { - let win = getWindow(el); - let visible = element.isVisible(el, win); - if (!visible) { - throw new ElementNotVisibleError("Element is not visible"); - } +this.Interactions = function(getCapabilies) { + this.accessibility = new Accessibility(getCapabilies); +}; - let a11y = accessibility.get(strict); - return a11y.getAccessible(el, true).then(acc => { - a11y.checkVisible(acc, el, visible); - - if (atom.isElementEnabled(el)) { - a11y.checkEnabled(acc, el, true); - a11y.checkActionable(acc, el); - - if (element.isXULElement(el)) { - el.click(); +Interactions.prototype = { + /** + * Send click event to element. + * + * @param nsIDOMWindow, ShadowRoot container + * The window and an optional shadow root that contains the element + * + * @param ElementManager elementManager + * + * @param String id + * The DOM reference ID + */ + clickElement(container, elementManager, id) { + let el = elementManager.getKnownElement(id, container); + let visible = element.isVisible(el); + if (!visible) { + throw new ElementNotVisibleError('Element is not visible'); + } + return this.accessibility.getAccessibleObject(el, true).then(acc => { + this.accessibility.checkVisible(acc, el, visible); + if (atom.isElementEnabled(el)) { + this.accessibility.checkEnabled(acc, el, true, container); + this.accessibility.checkActionable(acc, el); + if (element.isXULElement(el)) { + el.click(); + } else { + let rects = el.getClientRects(); + let win = el.ownerDocument.defaultView; + event.synthesizeMouseAtPoint( + rects[0].left + rects[0].width / 2, + rects[0].top + rects[0].height / 2, + {} /* opts */, + win); + } } else { - let rects = el.getClientRects(); - event.synthesizeMouseAtPoint( - rects[0].left + rects[0].width / 2, - rects[0].top + rects[0].height / 2, - {} /* opts */, - win); + throw new InvalidElementStateError('Element is not enabled'); } + }); + }, + /** + * Send keys to element + * + * @param nsIDOMWindow, ShadowRoot container + * The window and an optional shadow root that contains the element + * + * @param ElementManager elementManager + * + * @param String id + * The DOM reference ID + * + * @param String?Array value + * Value to send to an element + * + * @param Boolean ignoreVisibility + * A flag to check element visibility + */ + sendKeysToElement(container, elementManager, id, value, ignoreVisibility) { + let el = elementManager.getKnownElement(id, container); + return this.accessibility.getAccessibleObject(el, true).then(acc => { + this.accessibility.checkActionable(acc, el); + event.sendKeysToElement( + value, el, {ignoreVisibility: false}, container.frame); + }); + }, + + /** + * Determine the element displayedness of the given web element. + * + * @param nsIDOMWindow, ShadowRoot container + * The window and an optional shadow root that contains the element + * + * @param ElementManager elementManager + * + * @param {WebElement} id + * Reference to web element. + * + * Also performs additional accessibility checks if enabled by session + * capability. + */ + isElementDisplayed(container, elementManager, id) { + let el = elementManager.getKnownElement(id, container); + let displayed = atom.isElementDisplayed(el, container.frame); + return this.accessibility.getAccessibleObject(el).then(acc => { + this.accessibility.checkVisible(acc, el, displayed); + return displayed; + }); + }, + + /** + * Check if element is enabled. + * + * @param nsIDOMWindow, ShadowRoot container + * The window and an optional shadow root that contains the element + * + * @param ElementManager elementManager + * + * @param {WebElement} id + * Reference to web element. + * + * @return {boolean} + * True if enabled, false otherwise. + */ + isElementEnabled(container, elementManager, id) { + let el = elementManager.getKnownElement(id, container); + let enabled = true; + if (element.isXULElement(el)) { + // Check if XUL element supports disabled attribute + if (DISABLED_ATTRIBUTE_SUPPORTED_XUL.has(el.tagName.toUpperCase())) { + let disabled = atom.getElementAttribute(el, 'disabled', container.frame); + if (disabled && disabled === 'true') { + enabled = false; + } + } } else { - throw new InvalidElementStateError("Element is not enabled"); + enabled = atom.isElementEnabled(el, container.frame); } - }); -}; + return this.accessibility.getAccessibleObject(el).then(acc => { + this.accessibility.checkEnabled(acc, el, enabled, container); + return enabled; + }); + }, -/** - * Send keys to element. - * - * @param {DOMElement|XULElement} el - * Element to send key events to. - * @param {Array.} value - * Sequence of keystrokes to send to the element. - * @param {boolean} ignoreVisibility - * Flag to enable or disable element visibility tests. - * @param {boolean=} strict - * Enforce strict accessibility tests. - */ -interaction.sendKeysToElement = function(el, value, ignoreVisibility, strict = false) { - let win = getWindow(el); - let a11y = accessibility.get(strict); - return a11y.getAccessible(el, true).then(acc => { - a11y.checkActionable(acc, el); - event.sendKeysToElement(value, el, {ignoreVisibility: false}, win); - }); -}; - -/** - * Determine the element displayedness of an element. - * - * @param {DOMElement|XULElement} el - * Element to determine displayedness of. - * @param {boolean=} strict - * Enforce strict accessibility tests. - * - * @return {boolean} - * True if element is displayed, false otherwise. - */ -interaction.isElementDisplayed = function(el, strict = false) { - let win = getWindow(el); - let displayed = atom.isElementDisplayed(el, win); - - let a11y = accessibility.get(strict); - return a11y.getAccessible(el).then(acc => { - a11y.checkVisible(acc, el, displayed); - return displayed; - }); -}; - -/** - * Check if element is enabled. - * - * @param {DOMElement|XULElement} el - * Element to test if is enabled. - * - * @return {boolean} - * True if enabled, false otherwise. - */ -interaction.isElementEnabled = function(el, strict = false) { - let enabled = true; - let win = getWindow(el); - - if (element.isXULElement(el)) { - // check if XUL element supports disabled attribute - if (DISABLED_ATTRIBUTE_SUPPORTED_XUL.has(el.tagName.toUpperCase())) { - let disabled = atom.getElementAttribute(el, "disabled", win); - if (disabled && disabled === "true") { - enabled = false; + /** + * Determines if the referenced element is selected or not. + * + * This operation only makes sense on input elements of the Checkbox- + * and Radio Button states, or option elements. + * + * @param nsIDOMWindow, ShadowRoot container + * The window and an optional shadow root that contains the element + * + * @param ElementManager elementManager + * + * @param {WebElement} id + * Reference to web element. + */ + isElementSelected(container, elementManager, id) { + let el = elementManager.getKnownElement(id, container); + let selected = true; + if (element.isXULElement(el)) { + let tagName = el.tagName.toUpperCase(); + if (CHECKED_PROPERTY_SUPPORTED_XUL.has(tagName)) { + selected = el.checked; } + if (SELECTED_PROPERTY_SUPPORTED_XUL.has(tagName)) { + selected = el.selected; + } + } else { + selected = atom.isElementSelected(el, container.frame); } - } else { - enabled = atom.isElementEnabled(el, {frame: win}); - } - - let a11y = accessibility.get(strict); - return a11y.getAccessible(el).then(acc => { - a11y.checkEnabled(acc, el, enabled); - return enabled; - }); + return this.accessibility.getAccessibleObject(el).then(acc => { + this.accessibility.checkSelected(acc, el, selected); + return selected; + }); + }, }; - -/** - * Determines if the referenced element is selected or not. - * - * This operation only makes sense on input elements of the Checkbox- - * and Radio Button states, or option elements. - * - * @param {DOMElement|XULElement} el - * Element to test if is selected. - * @param {boolean=} strict - * Enforce strict accessibility tests. - * - * @return {boolean} - * True if element is selected, false otherwise. - */ -interaction.isElementSelected = function(el, strict = false) { - let selected = true; - let win = getWindow(el); - - if (element.isXULElement(el)) { - let tagName = el.tagName.toUpperCase(); - if (CHECKED_PROPERTY_SUPPORTED_XUL.has(tagName)) { - selected = el.checked; - } - if (SELECTED_PROPERTY_SUPPORTED_XUL.has(tagName)) { - selected = el.selected; - } - } else { - selected = atom.isElementSelected(el, win); - } - - let a11y = accessibility.get(strict); - return a11y.getAccessible(el).then(acc => { - a11y.checkSelected(acc, el, selected); - return selected; - }); -}; - -function getWindow(el) { - return el.ownerDocument.defaultView; -} diff --git a/testing/marionette/listener.js b/testing/marionette/listener.js index 72bef2673c5d..46b8d8d7883d 100644 --- a/testing/marionette/listener.js +++ b/testing/marionette/listener.js @@ -13,7 +13,6 @@ var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] loader.loadSubScript("chrome://marionette/content/simpletest.js"); loader.loadSubScript("chrome://marionette/content/common.js"); -Cu.import("chrome://marionette/content/accessibility.js"); Cu.import("chrome://marionette/content/action.js"); Cu.import("chrome://marionette/content/atom.js"); Cu.import("chrome://marionette/content/capture.js"); @@ -42,7 +41,9 @@ var isRemoteBrowser = () => curContainer.frame.contentWindow !== null; var previousContainer = null; var elementManager = new ElementManager(); +// Holds session capabilities. var capabilities = {}; +var interactions = new Interactions(() => capabilities); var actions = new action.Chain(checkForInterrupted); @@ -857,11 +858,9 @@ function singleTap(id, corx, cory) { if (!visible) { throw new ElementNotVisibleError("Element is not currently visible and may not be manipulated"); } - - let a11y = accessibility.get(capabilities.raisesAccessibilityExceptions); - return a11y.getAccessible(el, true).then(acc => { - a11y.checkVisible(acc, el, visible); - a11y.checkActionable(acc, el); + return interactions.accessibility.getAccessibleObject(el, true).then(acc => { + interactions.accessibility.checkVisible(acc, el, visible); + interactions.accessibility.checkActionable(acc, el); if (!curContainer.frame.document.createTouch) { actions.mouseEventsOnly = true; } @@ -1270,9 +1269,7 @@ function getActiveElement() { * Reference to the web element to click. */ function clickElement(id) { - let el = elementManager.getKnownElement(id, curContainer); - return interaction.clickElement( - el, capabilities.raisesAccessibilityExceptions); + return interactions.clickElement(curContainer, elementManager, id); } /** @@ -1326,9 +1323,7 @@ function getElementTagName(id) { * capability. */ function isElementDisplayed(id) { - let el = elementManager.getKnownElement(id, curContainer); - return interaction.isElementDisplayed( - el, capabilities.raisesAccessibilityExceptions); + return interactions.isElementDisplayed(curContainer, elementManager, id); } /** @@ -1379,9 +1374,7 @@ function getElementRect(id) { * True if enabled, false otherwise. */ function isElementEnabled(id) { - let el = elementManager.getKnownElement(id, curContainer); - return interaction.isElementEnabled( - el, capabilities.raisesAccessibilityExceptions); + return interactions.isElementEnabled(curContainer, elementManager, id); } /** @@ -1391,9 +1384,7 @@ function isElementEnabled(id) { * and Radio Button states, or option elements. */ function isElementSelected(id) { - let el = elementManager.getKnownElement(id, curContainer); - return interaction.isElementSelected( - el, capabilities.raisesAccessibilityExceptions); + return interactions.isElementSelected(curContainer, elementManager, id); } /** @@ -1414,10 +1405,9 @@ function sendKeysToElement(msg) { sendSyncMessage("Marionette:getFiles", {value: p, command_id: command_id}); } else { - let promise = interaction.sendKeysToElement( - el, val, false, capabilities.raisesAccessibilityExceptions) - .then(() => sendOk(command_id)) - .catch(e => sendError(e, command_id)); + interactions.sendKeysToElement(curContainer, elementManager, id, val) + .then(() => sendOk(command_id)) + .catch(e => sendError(e, command_id)); } }