Bug 1546555 - make AccessibleActor form data a smallest set of accessible data necessary for the a11y panel to work. Allow for fetching full accessible object data via new hydrate method. r=nchevobbe
authorYura Zenevich <yura.zenevich@gmail.com>
Sat, 27 Apr 2019 13:33:17 +0000
changeset 530470 660c16b89380269752e7e67ed717a4cf5dca6e5f
parent 530469 bdc963e3823832db9bc80b35be67206c25e33381
child 530471 70a7b7db7b84452a83026949fccda6d10ca57869
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnchevobbe
bugs1546555
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 1546555 - make AccessibleActor form data a smallest set of accessible data necessary for the a11y panel to work. Allow for fetching full accessible object data via new hydrate method. r=nchevobbe Differential Revision: https://phabricator.services.mozilla.com/D28752
devtools/client/accessibility/accessibility-startup.js
devtools/client/accessibility/accessibility-view.js
devtools/client/accessibility/actions/details.js
devtools/client/accessibility/panel.js
devtools/server/actors/accessibility/accessible.js
devtools/server/tests/browser/browser_accessibility_node.js
devtools/server/tests/browser/browser_accessibility_node_events.js
devtools/server/tests/browser/browser_accessibility_walker_audit.js
devtools/shared/fronts/accessibility.js
devtools/shared/specs/accessibility.js
--- a/devtools/client/accessibility/accessibility-startup.js
+++ b/devtools/client/accessibility/accessibility-startup.js
@@ -51,20 +51,22 @@ class AccessibilityStartup {
       this._supports.enableDisable =
         await this.target.actorHasMethod("accessibility", "enable");
 
       if (this._supports.enableDisable) {
         ([
           this._supports.relations,
           this._supports.snapshot,
           this._supports.audit,
+          this._supports.hydration,
         ] = await Promise.all([
           this.target.actorHasMethod("accessible", "getRelations"),
           this.target.actorHasMethod("accessible", "snapshot"),
           this.target.actorHasMethod("accessible", "audit"),
+          this.target.actorHasMethod("accessible", "hydrate"),
         ]));
 
         await this._accessibility.bootstrap();
       }
 
       return true;
     } catch (e) {
       // toolbox may be destroyed during this step.
--- a/devtools/client/accessibility/accessibility-view.js
+++ b/devtools/client/accessibility/accessibility-view.js
@@ -74,28 +74,38 @@ AccessibilityView.prototype = {
     window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED);
   },
 
   async highlightAccessible(walker, accessible) {
     await this.store.dispatch(highlight(walker, accessible));
     window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_HIGHLIGHTED);
   },
 
-  async selectNodeAccessible(walker, node) {
+  async selectNodeAccessible(walker, node, supports) {
     let accessible = await walker.getAccessibleFor(node);
+    if (accessible && supports.hydration) {
+      await accessible.hydrate();
+    }
+
     // If node does not have an accessible object, try to find node's child text node and
     // try to retrieve an accessible object for that child instead. This is the best
     // effort approach until there's accessibility API to retrieve accessible object at
     // point.
     if (!accessible || accessible.indexInParent < 0) {
       const { nodes: children } = await gToolbox.walker.children(node);
       for (const child of children) {
         if (child.nodeType === nodeConstants.TEXT_NODE) {
           accessible = await walker.getAccessibleFor(child);
-          if (accessible && accessible.indexInParent >= 0) {
+          // indexInParent property is only available with additional request
+          // for data (hydration) about the accessible object.
+          if (accessible && supports.hydration) {
+            await accessible.hydrate();
+          }
+
+          if (accessible.indexInParent >= 0) {
             break;
           }
         }
       }
     }
 
     await this.store.dispatch(select(walker, accessible));
     window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_HIGHLIGHTED);
--- a/devtools/client/accessibility/actions/details.js
+++ b/devtools/client/accessibility/actions/details.js
@@ -12,10 +12,11 @@ const { UPDATE_DETAILS } = require("../c
  * @param {Object} accessible front
  * @param {Object} list of supported serverside features.
  */
 exports.updateDetails = (domWalker, accessible, supports) =>
   dispatch => Promise.all([
     domWalker.getNodeFromActor(accessible.actorID, ["rawAccessible", "DOMNode"]),
     supports.relations ? accessible.getRelations() : [],
     supports.audit ? accessible.audit() : {},
+    supports.hydration ? accessible.hydrate() : null,
   ]).then(response => dispatch({ accessible, type: UPDATE_DETAILS, response }))
     .catch(error => dispatch({ accessible, type: UPDATE_DETAILS, error }));
--- a/devtools/client/accessibility/panel.js
+++ b/devtools/client/accessibility/panel.js
@@ -149,17 +149,18 @@ AccessibilityPanel.prototype = {
   },
 
   selectAccessibleForNode(nodeFront, reason) {
     if (reason) {
       this._telemetry.keyedScalarAdd(
         "devtools.accessibility.select_accessible_for_node", reason, 1);
     }
 
-    this.postContentMessage("selectNodeAccessible", this.walker, nodeFront);
+    this.postContentMessage("selectNodeAccessible", this.walker, nodeFront,
+      this.supports);
   },
 
   highlightAccessible(accessibleFront) {
     this.postContentMessage("highlightAccessible", this.walker, accessibleFront);
   },
 
   postContentMessage(type, ...args) {
     const event = new this.panelWin.MessageEvent("devtools/chrome/message", {
--- a/devtools/server/actors/accessibility/accessible.js
+++ b/devtools/server/actors/accessibility/accessible.js
@@ -361,26 +361,39 @@ const AccessibleActor = ActorClassWithSp
     return relationObjects;
   },
 
   form() {
     return {
       actor: this.actorID,
       role: this.role,
       name: this.name,
+      childCount: this.childCount,
+      checks: this._lastAudit,
+    };
+  },
+
+  /**
+   * Provide additional (full) information about the accessible object that is
+   * otherwise missing from the form.
+   *
+   * @return {Object}
+   *         Object that contains accessible object information such as states,
+   *         actions, attributes, etc.
+   */
+  hydrate() {
+    return {
       value: this.value,
       description: this.description,
       keyboardShortcut: this.keyboardShortcut,
-      childCount: this.childCount,
       domNodeType: this.domNodeType,
       indexInParent: this.indexInParent,
       states: this.states,
       actions: this.actions,
       attributes: this.attributes,
-      checks: this._lastAudit,
     };
   },
 
   _isValidTextLeaf(rawAccessible) {
     return !isDefunct(rawAccessible) &&
            TEXT_ROLES.has(rawAccessible.role) &&
            rawAccessible.name && rawAccessible.name.trim().length > 0;
   },
--- a/devtools/server/tests/browser/browser_accessibility_node.js
+++ b/devtools/server/tests/browser/browser_accessibility_node.js
@@ -14,16 +14,24 @@ add_task(async function() {
   const a11yWalker = await accessibility.getWalker();
   await accessibility.enable();
   const buttonNode = await walker.querySelector(walker.rootNode, "#button");
   const accessibleFront = await a11yWalker.getAccessibleFor(buttonNode);
 
   checkA11yFront(accessibleFront, {
     name: "Accessible Button",
     role: "pushbutton",
+    childCount: 1,
+  });
+
+  await accessibleFront.hydrate();
+
+  checkA11yFront(accessibleFront, {
+    name: "Accessible Button",
+    role: "pushbutton",
     value: "",
     description: "Accessibility Test",
     keyboardShortcut: modifiers + "b",
     childCount: 1,
     domNodeType: 1,
     indexInParent: 1,
     states: ["focusable", "selectable text", "opaque", "enabled", "sensitive"],
     actions: [ "Press" ],
--- a/devtools/server/tests/browser/browser_accessibility_node_events.js
+++ b/devtools/server/tests/browser/browser_accessibility_node_events.js
@@ -19,16 +19,24 @@ add_task(async function() {
   const accessibleFront = await a11yWalker.getAccessibleFor(buttonNode);
   const sliderNode = await walker.querySelector(walker.rootNode, "#slider");
   const accessibleSliderFront = await a11yWalker.getAccessibleFor(sliderNode);
   const browser = gBrowser.selectedBrowser;
 
   checkA11yFront(accessibleFront, {
     name: "Accessible Button",
     role: "pushbutton",
+    childCount: 1,
+  });
+
+  await accessibleFront.hydrate();
+
+  checkA11yFront(accessibleFront, {
+    name: "Accessible Button",
+    role: "pushbutton",
     value: "",
     description: "Accessibility Test",
     keyboardShortcut: modifiers + "b",
     childCount: 1,
     domNodeType: 1,
     indexInParent: 1,
     states: ["focusable", "selectable text", "opaque", "enabled", "sensitive"],
     actions: [ "Press" ],
@@ -87,25 +95,27 @@ add_task(async function() {
         "text-align": "center",
         "text-indent": "0px",
       }});
       is(newAttrs.live, "polite", "Attributes are updated");
     }, () => ContentTask.spawn(browser, null, () =>
       content.document.getElementById("button").setAttribute("aria-live", "polite")));
 
   info("Value change event");
+  await accessibleSliderFront.hydrate();
   checkA11yFront(accessibleSliderFront, { value: "5" });
   await emitA11yEvent(accessibleSliderFront, "value-change",
     () => checkA11yFront(accessibleSliderFront, { value: "6" }),
     () => ContentTask.spawn(browser, null, () =>
       content.document.getElementById("slider").setAttribute("aria-valuenow", "6")));
 
   info("Reorder event");
   is(accessibleSliderFront.childCount, 1, "Slider has only 1 child");
   const [firstChild ] = await accessibleSliderFront.children();
+  await firstChild.hydrate();
   is(firstChild.indexInParent, 0, "Slider's first child has correct index in parent");
   await emitA11yEvent(accessibleSliderFront, "reorder",
     childCount => {
       is(childCount, 2, "Child count is updated");
       is(accessibleSliderFront.childCount, 2, "Child count is updated");
       is(firstChild.indexInParent, 1,
         "Slider's first child has an updated index in parent");
     }, () => ContentTask.spawn(browser, null, () => {
--- a/devtools/server/tests/browser/browser_accessibility_walker_audit.js
+++ b/devtools/server/tests/browser/browser_accessibility_walker_audit.js
@@ -7,127 +7,52 @@
 // 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,
       },
     },
--- a/devtools/shared/fronts/accessibility.js
+++ b/devtools/shared/fronts/accessibility.js
@@ -80,17 +80,18 @@ class AccessibleFront extends FrontClass
   }
 
   get checks() {
     return this._form.checks;
   }
 
   form(form) {
     this.actorID = form.actor;
-    this._form = form;
+    this._form = this._form || {};
+    Object.assign(this._form, form);
   }
 
   nameChange(name, parent, walker) {
     this._form.name = name;
     // Name change event affects the tree rendering, we fire this event on
     // accessibility walker as the point of interaction for UI.
     if (walker) {
       events.emit(walker, "name-change", this, parent);
@@ -140,16 +141,22 @@ class AccessibleFront extends FrontClass
 
   attributesChange(attributes) {
     this._form.attributes = attributes;
   }
 
   audited(checks) {
     this._form.checks = checks;
   }
+
+  hydrate() {
+    return super.hydrate().then(properties => {
+      Object.assign(this._form, properties);
+    });
+  }
 }
 
 class AccessibleWalkerFront extends FrontClassWithSpec(accessibleWalkerSpec) {
   constructor(client) {
     super(client);
     this.before("accessible-destroy", this.accessibleDestroy.bind(this));
   }
 
--- a/devtools/shared/specs/accessibility.js
+++ b/devtools/shared/specs/accessibility.js
@@ -97,16 +97,22 @@ const accessibleSpec = generateActorSpec
       },
     },
     getRelations: {
       request: {},
       response: {
         relations: RetVal("array:accessibleRelation"),
       },
     },
+    hydrate: {
+      request: {},
+      response: {
+        properties: RetVal("json"),
+      },
+    },
     snapshot: {
       request: {},
       response: {
         snapshot: RetVal("json"),
       },
     },
   },
 });