Bug 1507870 - add support for taking a snapshot of the subtree of the accessible object. r=pbro
authorYura Zenevich <yura.zenevich@gmail.com>
Fri, 30 Nov 2018 14:55:12 +0000
changeset 508200 aa273e2036815104cb170ec0acf522aa4788e8e2
parent 508199 7b2786f1874312c62136e47722ffa55911fe8a4f
child 508201 5056bbebdb71a5d258f0173b4e39d7b4e16275c0
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1507870
milestone65.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 1507870 - add support for taking a snapshot of the subtree of the accessible object. r=pbro MozReview-Commit-ID: JdZe0N3ot4c Differential Revision: https://phabricator.services.mozilla.com/D12502
devtools/server/actors/accessibility/accessible.js
devtools/server/tests/browser/browser_accessibility_node.js
devtools/shared/specs/accessibility.js
--- a/devtools/server/actors/accessibility/accessible.js
+++ b/devtools/server/actors/accessibility/accessible.js
@@ -1,30 +1,117 @@
 /* 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 { Ci } = require("chrome");
+const { Ci, Cu } = require("chrome");
 const { Actor, ActorClassWithSpec } = require("devtools/shared/protocol");
 const { accessibleSpec } = require("devtools/shared/specs/accessibility");
 
 loader.lazyRequireGetter(this, "getContrastRatioFor", "devtools/server/actors/utils/accessibility", true);
 loader.lazyRequireGetter(this, "isDefunct", "devtools/server/actors/utils/accessibility", true);
+loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
 
 const nsIAccessibleRelation = Ci.nsIAccessibleRelation;
 const RELATIONS_TO_IGNORE = new Set([
   nsIAccessibleRelation.RELATION_CONTAINING_APPLICATION,
   nsIAccessibleRelation.RELATION_CONTAINING_TAB_PANE,
   nsIAccessibleRelation.RELATION_CONTAINING_WINDOW,
   nsIAccessibleRelation.RELATION_PARENT_WINDOW_OF,
   nsIAccessibleRelation.RELATION_SUBWINDOW_OF,
 ]);
 
+const STATE_DEFUNCT = Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT;
+const CSS_TEXT_SELECTOR = "#text";
+
+/**
+ * Get node inforamtion such as nodeType and the unique CSS selector for the node.
+ * @param  {DOMNode} node
+ *         Node for which to get the information.
+ * @return {Object}
+ *         Information about the type of the node and how to locate it.
+ */
+function getNodeDescription(node) {
+  if (!node || Cu.isDeadWrapper(node)) {
+    return { nodeType: undefined, nodeCssSelector: "" };
+  }
+
+  const { nodeType } = node;
+  return {
+    nodeType,
+    // If node is a text node, we find a unique CSS selector for its parent and add a
+    // CSS_TEXT_SELECTOR postfix to indicate that it's a text node.
+    nodeCssSelector: nodeType === Node.TEXT_NODE ?
+      `${findCssSelector(node.parentNode)}${CSS_TEXT_SELECTOR}` :
+      findCssSelector(node),
+  };
+}
+
+/**
+ * Get a snapshot of the nsIAccessible object including its subtree. None of the subtree
+ * queried here is cached via accessible walker's refMap.
+ * @param  {nsIAccessible} acc
+ *         Accessible object to take a snapshot of.
+ * @param  {nsIAccessibilityService} a11yService
+ *         Accessibility service instance in the current process, used to get localized
+ *         string representation of various accessible properties.
+ * @return {JSON}
+ *         JSON snapshot of the accessibility tree with root at current accessible.
+ */
+function getSnapshot(acc, a11yService) {
+  if (isDefunct(acc)) {
+    return {
+      states: [ a11yService.getStringStates(0, STATE_DEFUNCT) ],
+    };
+  }
+
+  const actions = [];
+  for (let i = 0; i < acc.actionCount; i++) {
+    actions.push(acc.getActionDescription(i));
+  }
+
+  const attributes = {};
+  if (acc.attributes) {
+    for (const { key, value } of acc.attributes.enumerate()) {
+      attributes[key] = value;
+    }
+  }
+
+  const state = {};
+  const extState = {};
+  acc.getState(state, extState);
+  const states = [
+    ...a11yService.getStringStates(state.value, extState.value),
+  ];
+
+  const children = [];
+  for (let child = acc.firstChild; child; child = child.nextSibling) {
+    children.push(getSnapshot(child, a11yService));
+  }
+
+  const { nodeType, nodeCssSelector } = getNodeDescription(acc.DOMNode);
+  return {
+    name: acc.name,
+    role: a11yService.getStringRole(acc.role),
+    actions,
+    value: acc.value,
+    nodeCssSelector,
+    nodeType,
+    description: acc.description,
+    keyboardShortcut: acc.accessKey || acc.keyboardShortcut,
+    childCount: acc.childCount,
+    indexInParent: acc.indexInParent,
+    states,
+    children,
+    attributes,
+  };
+}
+
 /**
  * The AccessibleActor provides information about a given accessible object: its
  * role, name, states, etc.
  */
 const AccessibleActor = ActorClassWithSpec(accessibleSpec, {
   initialize(walker, rawAccessible) {
     Actor.prototype.initialize.call(this, walker.conn);
     this.walker = walker;
@@ -311,11 +398,15 @@ const AccessibleActor = ActorClassWithSp
    * @return {Object|null}
    *         Audit results for the accessible object.
   */
   get audit() {
     return this.isDefunct ? null : {
       contrastRatio: this._getContrastRatio(),
     };
   },
+
+  snapshot() {
+    return getSnapshot(this.rawAccessible, this.walker.a11yService);
+  },
 });
 
 exports.AccessibleActor = AccessibleActor;
--- a/devtools/server/tests/browser/browser_accessibility_node.js
+++ b/devtools/server/tests/browser/browser_accessibility_node.js
@@ -61,13 +61,49 @@ add_task(async function() {
   is(relations[0].targets[0], controlAccessibleFront,
      "Label is a label for control accessible front");
   is(relations[1].type, "containing document",
      "Label has a containing document relation");
   is(relations[1].targets.length, 1, "Label is contained by just one document");
   is(relations[1].targets[0], docAccessibleFront,
      "Label's containing document is a root document");
 
+  info("Snapshot");
+  const snapshot = await controlAccessibleFront.snapshot();
+  Assert.deepEqual(snapshot, {
+    "name": "Label",
+    "role": "entry",
+    "actions": [ "Activate" ],
+    "value": "",
+    "nodeCssSelector": "#control",
+    "nodeType": 1,
+    "description": "",
+    "keyboardShortcut": "",
+    "childCount": 0,
+    "indexInParent": 1,
+    "states": [
+      "focusable",
+      "autocompletion",
+      "editable",
+      "opaque",
+      "single line",
+      "enabled",
+      "sensitive",
+    ],
+    "children": [],
+    "attributes": {
+      "margin-left": "0px",
+      "text-align": "start",
+      "text-indent": "0px",
+      "id": "control",
+      "tag": "input",
+      "margin-top": "0px",
+      "margin-bottom": "0px",
+      "margin-right": "0px",
+      "display": "inline",
+      "explicit-name": "true",
+    }});
+
   await accessibility.disable();
   await waitForA11yShutdown();
   await target.destroy();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/shared/specs/accessibility.js
+++ b/devtools/shared/specs/accessibility.js
@@ -87,16 +87,22 @@ const accessibleSpec = generateActorSpec
       },
     },
     getRelations: {
       request: {},
       response: {
         relations: RetVal("array:accessibleRelation"),
       },
     },
+    snapshot: {
+      request: {},
+      response: {
+        snapshot: RetVal("json"),
+      },
+    },
   },
 });
 
 const accessibleWalkerSpec = generateActorSpec({
   typeName: "accessiblewalker",
 
   events: {
     "accessible-destroy": {