Bug 1341493 - Wait for accessibility element if accessibility doc is busy. r=automatedtester
authorBrendan Dahl <brendan.dahl@gmail.com>
Thu, 15 Jun 2017 14:53:14 -0700
changeset 412993 d220383ec899a25106b913433f8938dbfea96b5f
parent 412992 12caabbf2ba860021b1879c5cc9e7ce521aa373e
child 412994 73d1332a69f4dda882a17e413f40761fc44512aa
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester
bugs1341493
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1341493 - Wait for accessibility element if accessibility doc is busy. r=automatedtester Occasionally marionette tries to get the accessibility element while the elements are still being built. This causes getAccessibleFor to return null when there actually should be an accessibility element available. Instead, if the document is busy, wait until it finishes to get the accessibility element.
testing/marionette/accessibility.js
testing/marionette/harness/marionette_harness/tests/unit/test_accessibility.py
testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
--- a/testing/marionette/accessibility.js
+++ b/testing/marionette/accessibility.js
@@ -1,16 +1,17 @@
 /* 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/Services.jsm');
 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(
@@ -27,29 +28,16 @@ XPCOMUtils.defineLazyGetter(this, "servi
     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
@@ -145,38 +133,57 @@ accessibility.Checks = class {
     }
 
     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(() => {
+      // First, check if accessibility is ready.
+      let docAcc = accessibility.service.getAccessibleFor(element.ownerDocument);
+      let state = {};
+      docAcc.getState(state, {});
+      if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0) {
+        // Accessibility is ready, resolve immediately.
+        let acc = accessibility.service.getAccessibleFor(element);
+        if (mustHaveAccessible && !acc) {
+          reject();
+        } else {
+          resolve(acc);
+        }
+        return;
+      }
+      // Accessibility for the doc is busy, so wait for the state to change.
+      let eventObserver = {
+        observe(subject, topic, data) {
+          if (topic !== "accessible-event") {
+            return;
+          }
+
+          let event = subject.QueryInterface(Ci.nsIAccessibleEvent);
+          // If event type does not match expected type, skip the event.
+          if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) {
+            return;
+          }
+          // If event's accessible does not match expected accessible, skip the event.
+          if (event.accessible !== docAcc) {
+            return;
+          }
+
+          Services.obs.removeObserver(this, "accessible-event");
           let acc = accessibility.service.getAccessibleFor(element);
-          if (acc || --attempts <= 0) {
-            clearInterval(intervalId);
-            if (acc) {
-              resolve(acc);
-            } else {
-              reject();
-            }
+          if (mustHaveAccessible && !acc) {
+            reject();
+          } else {
+            resolve(acc);
           }
-        }, GET_ACCESSIBLE_ATTEMPT_INTERVAL);
-      }
+        }
+      };
+      Services.obs.addObserver(eventObserver, "accessible-event");
     }).catch(() => this.error(
         "Element does not have an accessible object", element));
   };
 
   /**
    * Test if the accessible has a role that supports some arbitrary
    * action.
    *
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_accessibility.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_accessibility.py
@@ -147,18 +147,16 @@ class TestAccessibility(MarionetteTestCa
         self.setup_accessibility(False, True)
         # No exception should be raised
         self.run_element_test(self.invalid_elementIDs, lambda button: button.click())
         # Elements are invisible
         self.run_element_test(self.falsy_elements,
                               lambda button: self.assertRaises(ElementNotInteractableException,
                                                                button.click))
 
-    @unittest.skipIf(sys.platform.startswith("linux"),
-                     "Bug 1341493 - ElementNotAccessibleException not raised")
     def test_element_visible_but_not_visible_to_accessbility(self):
         self.setup_accessibility()
         # Elements are displayed but hidden from accessibility API
         self.run_element_test(self.displayed_but_a11y_hidden_elementIDs,
                               lambda element: self.assertRaises(ElementNotAccessibleException,
                                                                 element.is_displayed))
 
     def test_element_is_visible_to_accessibility(self):
--- a/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
@@ -1,15 +1,14 @@
 [test_marionette.py]
 [test_geckoinstance.py]
 [test_data_driven.py]
 [test_session.py]
 [test_capabilities.py]
 [test_accessibility.py]
-skip-if = headless # Bug 1294075 and 1341493
 [test_expectedfail.py]
 expected = fail
 [test_click.py]
 [test_click_chrome.py]
 skip-if = appname == 'fennec'
 [test_checkbox.py]
 [test_checkbox_chrome.py]
 skip-if = appname == 'fennec'