Bug 1540904 - added full page API for accessibility walker actor. r=pbro
authorYura Zenevich <yura.zenevich@gmail.com>
Wed, 10 Apr 2019 18:36:02 +0000
changeset 468861 17ee3af4102a80992f87148b5e1a52fa88d433c8
parent 468860 cff6e35cda2b04d31d986a80c3e91e91e2d8db06
child 468862 f5d262c8ae962a64dd0ca4916aa2c1638970ebba
push id112758
push userdvarga@mozilla.com
push dateThu, 11 Apr 2019 04:29:43 +0000
treeherdermozilla-inbound@c83226dfc38a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1540904
milestone68.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 1540904 - added full page API for accessibility walker actor. r=pbro Differential Revision: https://phabricator.services.mozilla.com/D26458
devtools/server/actors/accessibility/walker.js
devtools/server/tests/browser/browser.ini
devtools/server/tests/browser/browser_accessibility_walker_audit.js
devtools/server/tests/browser/doc_accessibility_audit.html
devtools/server/tests/browser/head.js
devtools/shared/specs/accessibility.js
--- a/devtools/server/actors/accessibility/walker.js
+++ b/devtools/server/actors/accessibility/walker.js
@@ -118,16 +118,37 @@ function isStale(accessible) {
   const extraState = {};
   accessible.getState({}, extraState);
   // extraState.value is a bitmask. We are applying bitwise AND to mask out
   // irrelevant states.
   return !!(extraState.value & Ci.nsIAccessibleStates.EXT_STATE_STALE);
 }
 
 /**
+ * Get accessibility audit starting with the passed accessible object as a root.
+ *
+ * @param {Object} acc
+ *        AccessibileActor to be used as the root for the audit.
+ * @param {Map} report
+ *        An accumulator map to be used to store audit information.
+ */
+function getAudit(acc, report) {
+  if (acc.isDefunct) {
+    return;
+  }
+
+  // Audit returns a promise, save the actual value in the report.
+  report.set(acc, acc.audit().then(result => report.set(acc, result)));
+
+  for (const child of acc.children()) {
+    getAudit(child, report);
+  }
+}
+
+/**
  * The AccessibleWalkerActor stores a cache of AccessibleActors that represent
  * accessible objects in a given document.
  *
  * It is also responsible for implicitely initializing and shutting down
  * accessibility engine by storing a reference to the XPCOM accessibility
  * service.
  */
 const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
@@ -362,16 +383,40 @@ const AccessibleWalkerActor = ActorClass
     } catch (error) {
       throw new Error(`Failed to get ancestor for ${accessible}: ${error}`);
     }
 
     return ancestry.map(parent => (
       { accessible: parent, children: parent.children() }));
   },
 
+  /**
+   * Run accessibility audit and return relevant ancestries for AccessibleActors
+   * that have non-empty audit checks.
+   *
+   * @return {Promise}
+   *         A promise that resolves when the audit is complete and all relevant
+   *         ancestries are calculated.
+   */
+  async audit() {
+    const doc = await this.getDocument();
+    const report = new Map();
+    getAudit(doc, report);
+    await Promise.all(report.values());
+
+    const ancestries = [];
+    for (const [acc, audit] of report.entries()) {
+      if (audit && Object.values(audit).filter(check => check != null).length > 0) {
+        ancestries.push(this.getAncestry(acc));
+      }
+    }
+
+    return Promise.all(ancestries);
+  },
+
   onHighlighterEvent: function(data) {
     this.emit("highlighter-event", data);
   },
 
   /**
    * Accessible event observer function.
    *
    * @param {Ci.nsIAccessibleEvent} subject
--- a/devtools/server/tests/browser/browser.ini
+++ b/devtools/server/tests/browser/browser.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
   animation.html
   animation-data.html
+  doc_accessibility_audit.html
   doc_accessibility_infobar.html
   doc_accessibility.html
   doc_allocations.html
   doc_force_cc.html
   doc_force_gc.html
   doc_innerHTML.html
   doc_perf.html
   doc_promise-get-allocation-stack.html
@@ -42,16 +43,17 @@ skip-if = (os == 'win' && processor == '
 [browser_accessibility_infobar_show.js]
 [browser_accessibility_node.js]
 skip-if = (os == 'win' && processor == 'aarch64') # bug 1533184
 [browser_accessibility_node_audit.js]
 [browser_accessibility_node_events.js]
 skip-if = (os == 'win' && processor == 'aarch64') # bug 1533184
 [browser_accessibility_simple.js]
 skip-if = (os == 'win' && processor == 'aarch64') # bug 1533184
+[browser_accessibility_walker_audit.js]
 [browser_accessibility_walker.js]
 skip-if = (os == 'win' && processor == 'aarch64') # bug 1533487
 [browser_actor_error.js]
 [browser_animation_actor-lifetime.js]
 [browser_animation_emitMutations.js]
 [browser_animation_getProperties.js]
 [browser_animation_getMultipleStates.js]
 [browser_animation_getPlayers.js]
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_walker_audit.js
@@ -0,0 +1,163 @@
+/* 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";
+
+// Checks for the AccessibleWalkerActor audit.
+add_task(async function() {
+  const {target, accessibility} =
+    await initAccessibilityFrontForUrl(MAIN_DOMAIN + "doc_accessibility_audit.html");
+
+  const accessibles = [{
+    name: "",
+    role: "document",
+    value: "",
+    description: "",
+    keyboardShortcut: "",
+    childCount: 2,
+    domNodeType: 9,
+    indexInParent: 0,
+    states: [
+      "focused", "readonly", "focusable", "active", "opaque", "enabled", "sensitive",
+    ],
+    actions: [],
+    attributes: {
+      display: "block",
+      "explicit-name": "true",
+      "margin-bottom": "8px",
+      "margin-left": "8px",
+      "margin-right": "8px",
+      "margin-top": "8px",
+      tag: "body",
+      "text-align": "start",
+      "text-indent": "0px",
+    },
+    checks: {
+      "CONTRAST": null,
+    },
+  }, {
+    name: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
+          "eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+    role: "heading",
+    value: "",
+    description: "",
+    keyboardShortcut: "",
+    childCount: 1,
+    domNodeType: 1,
+    indexInParent: 0,
+    states: [ "selectable text", "opaque", "enabled", "sensitive" ],
+    actions: [],
+    attributes: {
+      display: "block",
+      formatting: "block",
+      id: "h1",
+      level: "1",
+      "margin-bottom": "21.4333px",
+      "margin-left": "0px",
+      "margin-right": "0px",
+      "margin-top": "21.4333px",
+      tag: "h1",
+      "text-align": "start",
+      "text-indent": "0px",
+    },
+    checks: {
+      "CONTRAST": null,
+    },
+  }, {
+    name: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
+           "eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+    role: "text leaf",
+    value: "",
+    description: "",
+    keyboardShortcut: "",
+    childCount: 0,
+    domNodeType: 3,
+    indexInParent: 0,
+    states: [ "opaque", "enabled", "sensitive" ],
+    actions: [],
+    attributes: { "explicit-name": "true" },
+    checks: {
+      "CONTRAST": {
+        "value": 21,
+        "color": [0, 0, 0, 1],
+        "backgroundColor": [255, 255, 255, 1],
+        "isLargeText": true,
+      },
+    },
+  }, {
+    name: "",
+    role: "paragraph",
+    value: "",
+    description: "",
+    keyboardShortcut: "",
+    childCount: 1,
+    domNodeType: 1,
+    indexInParent: 1,
+    states: [ "selectable text", "opaque", "enabled", "sensitive" ],
+    actions: [ "Press" ],
+    attributes: {
+      display: "block",
+      formatting: "block",
+      id: "p",
+      "margin-bottom": "16px",
+      "margin-left": "0px",
+      "margin-right": "0px",
+      "margin-top": "16px",
+      tag: "p",
+      "text-align": "start",
+      "text-indent": "0px",
+    },
+    checks: {
+      "CONTRAST": null,
+    },
+  }, {
+    name: "Accessible Paragraph",
+    role: "text leaf",
+    value: "",
+    description: "",
+    keyboardShortcut: "",
+    childCount: 0,
+    domNodeType: 3,
+    indexInParent: 0,
+    states: [ "opaque", "enabled", "sensitive" ],
+    actions: [],
+    attributes: { "explicit-name": "true" },
+    checks: {
+      "CONTRAST": {
+        "value": 21,
+        "color": [0, 0, 0, 1],
+        "backgroundColor": [255, 255, 255, 1],
+        "isLargeText": false,
+      },
+    },
+  }];
+
+  function findAccessible(name, role) {
+    return accessibles.find(accessible =>
+      accessible.name === name && accessible.role === role);
+  }
+
+  const a11yWalker = await accessibility.getWalker();
+  ok(a11yWalker, "The AccessibleWalkerFront was returned");
+  await accessibility.enable();
+
+  info("Checking AccessibleWalker audit functionality");
+  const ancestries = await a11yWalker.audit();
+
+  for (const ancestry of ancestries) {
+    for (const { accessible, children } of ancestry) {
+      checkA11yFront(accessible,
+                     findAccessible(accessibles.name, accessibles.role));
+      for (const child of children) {
+        checkA11yFront(child,
+                       findAccessible(child.name, child.role));
+      }
+    }
+  }
+
+  await accessibility.disable();
+  await waitForA11yShutdown();
+  await target.destroy();
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility_audit.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf-8">
+  </head>
+<body>
+  <h1 id="h1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</h1>
+  <p id="p">Accessible Paragraph</p>
+</body>
+</html>
--- a/devtools/server/tests/browser/head.js
+++ b/devtools/server/tests/browser/head.js
@@ -267,17 +267,17 @@ async function emitA11yEvent(emitter, na
 function checkA11yFront(front, expected, expectedFront) {
   ok(front, "The accessibility front is created");
 
   if (expectedFront) {
     is(front, expectedFront, "Matching accessibility front");
   }
 
   for (const key in expected) {
-    if (["actions", "states", "attributes"].includes(key)) {
+    if (["actions", "states", "attributes", "checks"].includes(key)) {
       SimpleTest.isDeeply(front[key], expected[key],
         `Accessible Front has correct ${key}`);
     } else {
       is(front[key], expected[key], `accessibility front has correct ${key}`);
     }
   }
 }
 
--- a/devtools/shared/specs/accessibility.js
+++ b/devtools/shared/specs/accessibility.js
@@ -157,16 +157,22 @@ const accessibleWalkerSpec = generateAct
       },
     },
     getAncestry: {
       request: { accessible: Arg(0, "accessible") },
       response: {
         ancestry: RetVal("array:accessibleWithChildren"),
       },
     },
+    audit: {
+      request: {},
+      response: {
+        audit: RetVal("array:array:accessibleWithChildren"),
+      },
+    },
     highlightAccessible: {
       request: {
         accessible: Arg(0, "accessible"),
         options: Arg(1, "nullable:json"),
       },
       response: {
         value: RetVal("nullable:boolean"),
       },