Bug 1245064 - Refactor interaction- and accessibility API; r=automatedtester a=test-only
authorAndreas Tolfsen <ato@mozilla.com>
Thu, 03 Mar 2016 13:58:13 +0000
changeset 325288 4857cfc29b009962d22ebd3da66fb865e194558c
parent 325287 7055469046c500356fa7d01212df745ea6da11d2
child 325289 a09a88275612bb12798c0f93b0c0063b1990be4e
push id1128
push userjlund@mozilla.com
push dateWed, 01 Jun 2016 01:31:59 +0000
treeherdermozilla-release@fe0d30de989d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester, test-only
bugs1245064
milestone47.0a2
Bug 1245064 - Refactor interaction- and accessibility API; r=automatedtester a=test-only MozReview-Commit-ID: LblRZz8KqPx
testing/marionette/accessibility.js
testing/marionette/driver.js
testing/marionette/interaction.js
testing/marionette/listener.js
--- a/testing/marionette/accessibility.js
+++ b/testing/marionette/accessibility.js
@@ -1,339 +1,417 @@
 /* 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/. */
 
-/* global Accessibility, Components, Log, ElementNotAccessibleError,
-          XPCOMUtils */
+"use strict";
+
+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");
 
-this.EXPORTED_SYMBOLS = ['Accessibility'];
+XPCOMUtils.defineLazyGetter(this, "retrieval",
+    () => Cc["@mozilla.org/accessibleRetrieval;1"].getService(Ci.nsIAccessibleRetrieval));
+
+this.EXPORTED_SYMBOLS = ["accessibility"];
+
+const logger = Log.repository.getLogger("Marionette");
 
 /**
- * Accessible states used to check element's state from the accessiblity API
- * perspective.
+ * 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 states = {
-  unavailable: Ci.nsIAccessibleStates.STATE_UNAVAILABLE,
-  focusable: Ci.nsIAccessibleStates.STATE_FOCUSABLE,
-  selectable: Ci.nsIAccessibleStates.STATE_SELECTABLE,
-  selected: Ci.nsIAccessibleStates.STATE_SELECTED
-};
+const GET_ACCESSIBLE_ATTEMPTS = 100;
 
-var logger = Log.repository.getLogger('Marionette');
+/**
+ * An interval between attempts to retrieve an accessible object for an
+ * element.
+ */
+const GET_ACCESSIBLE_ATTEMPT_INTERVAL = 10;
+
+this.accessibility = {};
 
 /**
- * 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.
- *
- * @param {Function} getCapabilies
- *        Session capabilities getter.
+ * Accessible states used to check element"s state from the accessiblity API
+ * perspective.
  */
-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.State = {
+  Unavailable: Ci.nsIAccessibleStates.STATE_UNAVAILABLE,
+  Focusable: Ci.nsIAccessibleStates.STATE_FOCUSABLE,
+  Selectable: Ci.nsIAccessibleStates.STATE_SELECTABLE,
+  Selected: Ci.nsIAccessibleStates.STATE_SELECTED,
 };
 
-Accessibility.prototype = {
+/**
+ * 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",
+]);
 
-  /**
-   * 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}
-   */
-  GET_ACCESSIBLE_ATTEMPTS: 100,
+
+/**
+ * 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 {
 
   /**
-   * An interval between attempts to retrieve an accessible object for an
-   * element.
-   * @type {Number} ms
-   */
-  GET_ACCESSIBLE_ATTEMPT_INTERVAL: 10,
-
-  /**
-   * Accessible object roles that support some action
-   * @type Object
+   * @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.
    */
-  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'
-  ]),
+  constructor(strict) {
+    this.strict = strict;
+  }
 
   /**
-   * 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
+   * 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.
    */
-  getAccessibleObject(element, mustHaveAccessible = false) {
+  getAccessible(element, mustHaveAccessible = false) {
     return new Promise((resolve, reject) => {
-      let acc = this.retrieval.getAccessibleFor(element);
+      let acc = 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 {
-        // 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 attempts = GET_ACCESSIBLE_ATTEMPTS;
         let intervalId = setInterval(() => {
-          let acc = this.retrieval.getAccessibleFor(element);
+          let acc = retrieval.getAccessibleFor(element);
           if (acc || --attempts <= 0) {
             clearInterval(intervalId);
-            if (acc) { resolve(acc); }
-            else { reject(); }
+            if (acc) {
+              resolve(acc);
+            } else {
+              reject();
+            }
           }
-        }, this.GET_ACCESSIBLE_ATTEMPT_INTERVAL);
+        }, GET_ACCESSIBLE_ATTEMPT_INTERVAL);
       }
     }).catch(() => this.error(
-      'Element does not have an accessible object', element));
-  },
+        "Element does not have an accessible object", element));
+  };
 
   /**
-   * Check if the accessible has a role that supports some action
-   * @param nsIAccessible object
-   * @return Boolean an indicator of role being actionable
+   * 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 this.ACTIONABLE_ROLES.has(
-      this.retrieval.getStringRole(accessible.role));
-  },
+    return accessibility.ActionableRoles.has(
+        retrieval.getStringRole(accessible.role));
+  }
 
   /**
-   * 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
+   * 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;
-  },
+  }
 
   /**
-   * Determine if an accessible has a valid name
-   * @param nsIAccessible object
-   * @return Boolean an indicator that the element has a non empty valid name
+   * 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();
-  },
+  }
 
   /**
-   * 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
+   * 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');
+      hidden = accessible.attributes.getStringProperty("hidden");
     } finally {
-      // If the property is missing, exception will be thrown.
-      return hidden && hidden === 'true';
+      // if the property is missing, error will be thrown
+      return hidden && hidden === "true";
     }
-  },
+  }
 
   /**
-   * Verify if an accessible has a given state
-   * @param nsIAccessible object
-   * @param Number stateToMatch the state to match
-   * @return Boolean accessible has a state
+   * 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);
-  },
+  }
 
   /**
-   * 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
+   * 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.
+   */
+  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);
+  }
 
   /**
-   * Send an error message or log the error message in the log
-   * @param String message
-   * @param DOMElement element that caused an error
+   * 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.
    */
   error(message, element) {
     if (!message) {
       return;
     }
     if (element) {
       let {id, tagName, className} = element;
       message += `: id: ${id}, tagName: ${tagName}, className: ${className}`;
     }
     if (this.strict) {
       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);
-  }
 };
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -153,18 +153,16 @@ this.GeckoDriver = function(appName, dev
 
     // proprietary extensions
     "XULappId" : Services.appinfo.ID,
     "appBuildId" : Services.appinfo.appBuildID,
     "device": device,
     "version": Services.appinfo.version,
   };
 
-  this.interactions = new Interactions(() => this.sessionCapabilities);
-
   this.mm = globalMessageManager;
   this.listener = proxy.toListener(() => this.mm, this.sendAsync.bind(this));
 
   // always keep weak reference to current dialogue
   this.dialog = null;
   let handleDialog = (subject, topic) => {
     let winr;
     if (topic == modal.COMMON_DIALOG_LOADED) {
@@ -1961,18 +1959,19 @@ GeckoDriver.prototype.getActiveElement =
  *     Reference ID to the element that will be clicked.
  */
 GeckoDriver.prototype.clickElement = function*(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      yield this.interactions.clickElement({ frame: win },
-        this.curBrowser.elementManager, id);
+      let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win});
+      yield interaction.clickElement(
+          el, this.sessionCapabilities.raisesAccessibilityExceptions);
       break;
 
     case Context.CONTENT:
       // We need to protect against the click causing an OOP frame to close.
       // This fires the mozbrowserclose event when it closes so we need to
       // listen for it and then just send an error back. The person making the
       // call should be aware something isnt right and handle accordingly
       this.addFrameCloseListener("click");
@@ -2060,18 +2059,20 @@ GeckoDriver.prototype.getElementTagName 
  *     Reference ID to the element that will be inspected.
  */
 GeckoDriver.prototype.isElementDisplayed = function*(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      resp.body.value = yield this.interactions.isElementDisplayed(
-        {frame: win}, this.curBrowser.elementManager, id);
+      let el = this.curBrowser.elementManager.getKnownElement(
+          id, {frame: win});
+      resp.body.value = yield interaction.isElementDisplayed(
+          el, this.sessionCapabilities.raisesAccessibilityExceptions);
       break;
 
     case Context.CONTENT:
       resp.body.value = yield this.listener.isElementDisplayed(id);
       break;
   }
 };
 
@@ -2108,18 +2109,20 @@ GeckoDriver.prototype.getElementValueOfC
  */
 GeckoDriver.prototype.isElementEnabled = function*(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       // Selenium atom doesn't quite work here
       let win = this.getCurrentWindow();
-      resp.body.value = yield this.interactions.isElementEnabled(
-        {frame: win}, this.curBrowser.elementManager, id);
+      let el = this.curBrowser.elementManager.getKnownElement(
+          id, {frame: win});
+      resp.body.value = yield interaction.isElementEnabled(
+          el, this.sessionCapabilities.raisesAccessibilityExceptions);
       break;
 
     case Context.CONTENT:
       resp.body.value = yield this.listener.isElementEnabled(id);
       break;
   }
 },
 
@@ -2131,18 +2134,20 @@ GeckoDriver.prototype.isElementEnabled =
  */
 GeckoDriver.prototype.isElementSelected = function*(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       // Selenium atom doesn't quite work here
       let win = this.getCurrentWindow();
-      resp.body.value = yield this.interactions.isElementSelected(
-        { frame: win }, this.curBrowser.elementManager, id);
+      let el = this.curBrowser.elementManager.getKnownElement(
+          id, {frame: win});
+      resp.body.value = yield interaction.isElementSelected(
+          el, this.sessionCapabilities.raisesAccessibilityExceptions);
       break;
 
     case Context.CONTENT:
       resp.body.value = yield this.listener.isElementSelected(id);
       break;
   }
 };
 
@@ -2181,18 +2186,20 @@ GeckoDriver.prototype.sendKeysToElement 
 
   if (!value) {
     throw new InvalidArgumentError(`Expected character sequence: ${value}`);
   }
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      yield this.interactions.sendKeysToElement(
-        { frame: win }, this.curBrowser.elementManager, id, value, true);
+      let el = this.curBrowser.elementManager.getKnownElement(
+          id, {frame: win});
+      yield interaction.sendKeysToElement(
+          el, value, true, this.sessionCapabilities.raisesAccessibilityExceptions);
       break;
 
     case Context.CONTENT:
       let err;
       let listener = function(msg) {
         this.mm.removeMessageListener("Marionette:setElementValue", listener);
 
         let val = msg.data.value;
--- a/testing/marionette/interaction.js
+++ b/testing/marionette/interaction.js
@@ -7,235 +7,227 @@
 const {utils: Cu} = Components;
 
 Cu.import("chrome://marionette/content/accessibility.js");
 Cu.import("chrome://marionette/content/atom.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/element.js");
 Cu.import("chrome://marionette/content/event.js");
 
-this.EXPORTED_SYMBOLS = ["Interactions"];
+this.EXPORTED_SYMBOLS = ["interaction"];
 
 /**
- * XUL elements that support disabled attribtue.
+ * XUL elements that support disabled attribute.
  */
 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 = {};
+
 /**
- * A collection of interactions available in marionette.
- * @type {Object}
+ * Interact with an element by clicking it.
+ *
+ * @param {DOMElement|XULElement} el
+ *     Element to click.
+ * @param {boolean=} strict
+ *     Enforce strict accessibility tests.
  */
-this.Interactions = function(getCapabilies) {
-  this.accessibility = new Accessibility(getCapabilies);
+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");
+  }
+
+  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();
+      } else {
+        let rects = el.getClientRects();
+        event.synthesizeMouseAtPoint(
+            rects[0].left + rects[0].width / 2,
+            rects[0].top + rects[0].height / 2,
+            {} /* opts */,
+            win);
+      }
+
+    } else {
+      throw new InvalidElementStateError("Element is not enabled");
+    }
+  });
+};
+
+/**
+ * Send keys to element.
+ *
+ * @param {DOMElement|XULElement} el
+ *     Element to send key events to.
+ * @param {Array.<string>} 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);
+  });
 };
 
-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');
+/**
+ * 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;
+      }
     }
-    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 {
-        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);
-    });
-  },
+  } else {
+    enabled = atom.isElementEnabled(el, {frame: win});
+  }
 
-  /**
-   * 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;
-    });
-  },
+  let a11y = accessibility.get(strict);
+  return a11y.getAccessible(el).then(acc => {
+    a11y.checkEnabled(acc, el, enabled);
+    return enabled;
+  });
+};
 
-  /**
-   * 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 {
-      enabled = atom.isElementEnabled(el, container.frame);
-    }
-    return this.accessibility.getAccessibleObject(el).then(acc => {
-      this.accessibility.checkEnabled(acc, el, enabled, container);
-      return enabled;
-    });
-  },
+/**
+ * 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);
 
-  /**
-   * 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);
+  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;
     }
-    return this.accessibility.getAccessibleObject(el).then(acc => {
-      this.accessibility.checkSelected(acc, el, selected);
-      return 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;
+}
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -8,16 +8,17 @@ var uuidGen = Cc["@mozilla.org/uuid-gene
     .getService(Ci.nsIUUIDGenerator);
 
 var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
     .getService(Ci.mozIJSSubScriptLoader);
 
 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");
 Cu.import("chrome://marionette/content/cookies.js");
 Cu.import("chrome://marionette/content/element.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
@@ -36,19 +37,17 @@ var marionetteTestName;
 var winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIDOMWindowUtils);
 var listenerId = null; // unique ID of this listener
 var curContainer = { frame: content, shadowRoot: null };
 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);
 
 // Contains the last file input element that was the target of
 // sendKeysToElement.
 var fileInputElement;
 
 // A dict of sandboxes used this session
@@ -853,19 +852,21 @@ function emitTouchEvent(type, touch) {
  */
 function singleTap(id, corx, cory) {
   let el = elementManager.getKnownElement(id, curContainer);
   // after this block, the element will be scrolled into view
   let visible = element.isVisible(el, corx, cory);
   if (!visible) {
     throw new ElementNotVisibleError("Element is not currently visible and may not be manipulated");
   }
-  return interactions.accessibility.getAccessibleObject(el, true).then(acc => {
-    interactions.accessibility.checkVisible(acc, el, visible);
-    interactions.accessibility.checkActionable(acc, el);
+
+  let a11y = accessibility.get(capabilities.raisesAccessibilityExceptions);
+  return a11y.getAccessible(el, true).then(acc => {
+    a11y.checkVisible(acc, el, visible);
+    a11y.checkActionable(acc, el);
     if (!curContainer.frame.document.createTouch) {
       actions.mouseEventsOnly = true;
     }
     let c = element.coordinates(el, corx, cory);
     if (!actions.mouseEventsOnly) {
       let touchId = actions.nextTouchId++;
       let touch = createATouch(el, c.x, c.y, touchId);
       emitTouchEvent('touchstart', touch);
@@ -1264,17 +1265,19 @@ function getActiveElement() {
 
 /**
  * Send click event to element.
  *
  * @param {WebElement} id
  *     Reference to the web element to click.
  */
 function clickElement(id) {
-  return interactions.clickElement(curContainer, elementManager, id);
+  let el = elementManager.getKnownElement(id, curContainer);
+  return interaction.clickElement(
+      el, capabilities.raisesAccessibilityExceptions);
 }
 
 /**
  * Get a given attribute of an element.
  *
  * @param {WebElement} id
  *     Reference to the web element to get the attribute of.
  * @param {string} name
@@ -1318,17 +1321,19 @@ function getElementTagName(id) {
 
 /**
  * Determine the element displayedness of the given web element.
  *
  * Also performs additional accessibility checks if enabled by session
  * capability.
  */
 function isElementDisplayed(id) {
-  return interactions.isElementDisplayed(curContainer, elementManager, id);
+  let el = elementManager.getKnownElement(id, curContainer);
+  return interaction.isElementDisplayed(
+      el, capabilities.raisesAccessibilityExceptions);
 }
 
 /**
  * Retrieves the computed value of the given CSS property of the given
  * web element.
  *
  * @param {String} id
  *     Web element reference.
@@ -1369,27 +1374,31 @@ function getElementRect(id) {
  *
  * @param {WebElement} id
  *     Reference to web element.
  *
  * @return {boolean}
  *     True if enabled, false otherwise.
  */
 function isElementEnabled(id) {
-  return interactions.isElementEnabled(curContainer, elementManager, id);
+  let el = elementManager.getKnownElement(id, curContainer);
+  return interaction.isElementEnabled(
+      el, capabilities.raisesAccessibilityExceptions);
 }
 
 /**
  * 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.
  */
 function isElementSelected(id) {
-  return interactions.isElementSelected(curContainer, elementManager, id);
+  let el = elementManager.getKnownElement(id, curContainer);
+  return interaction.isElementSelected(
+      el, capabilities.raisesAccessibilityExceptions);
 }
 
 /**
  * Send keys to element
  */
 function sendKeysToElement(msg) {
   let command_id = msg.json.command_id;
   let val = msg.json.value;
@@ -1400,19 +1409,20 @@ function sendKeysToElement(msg) {
     let p = val.join("");
         fileInputElement = el;
         // In e10s, we can only construct File objects in the parent process,
         // so pass the filename to driver.js, which in turn passes them back
         // to this frame script in receiveFiles.
         sendSyncMessage("Marionette:getFiles",
             {value: p, command_id: command_id});
   } else {
-    interactions.sendKeysToElement(curContainer, elementManager, id, val)
-        .then(() => sendOk(command_id))
-        .catch(e => sendError(e, command_id));
+    let promise = interaction.sendKeysToElement(
+        el, val, false, capabilities.raisesAccessibilityExceptions)
+      .then(() => sendOk(command_id))
+      .catch(e => sendError(e, command_id));
   }
 }
 
 /**
  * Clear the text of an element.
  */
 function clearElement(id) {
   try {