Bug 1151468 - add accessibility spec/actor/front to devtools. r=pbro
authorYura Zenevich <yura.zenevich@gmail.com>
Fri, 21 Jul 2017 13:15:06 -0400
changeset 378159 0f75865f19861841443ea3f6d80b9efbff2b3229
parent 378158 82052535770fac7ba6000e7c0c5e83b2fddf6101
child 378160 2cbfa0e50247c97457644f5d731534cbbfe27162
push id32422
push userarchaeopteryx@coole-files.de
push dateFri, 01 Sep 2017 08:39:53 +0000
treeherdermozilla-central@a3585c77e2b1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1151468
milestone57.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 1151468 - add accessibility spec/actor/front to devtools. r=pbro MozReview-Commit-ID: Ln1R8e04mGR
devtools/server/actors/accessibility.js
devtools/server/actors/moz.build
devtools/server/main.js
devtools/server/tests/browser/browser.ini
devtools/server/tests/browser/browser_accessibility_node.js
devtools/server/tests/browser/browser_accessibility_node_events.js
devtools/server/tests/browser/browser_accessibility_simple.js
devtools/server/tests/browser/browser_accessibility_walker.js
devtools/server/tests/browser/doc_accessibility.html
devtools/server/tests/browser/head.js
devtools/shared/fronts/accessibility.js
devtools/shared/fronts/moz.build
devtools/shared/specs/accessibility.js
devtools/shared/specs/moz.build
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/accessibility.js
@@ -0,0 +1,538 @@
+/* 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 { Cc, Ci, Cu } = require("chrome");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const Services = require("Services");
+const { Actor, ActorClassWithSpec } = require("devtools/shared/protocol");
+const defer = require("devtools/shared/defer");
+const events = require("devtools/shared/event-emitter");
+const {
+  accessibleSpec,
+  accessibleWalkerSpec,
+  accessibilitySpec
+} = require("devtools/shared/specs/accessibility");
+
+const nsIAccessibleEvent = Ci.nsIAccessibleEvent;
+const nsIAccessibleStateChangeEvent = Ci.nsIAccessibleStateChangeEvent;
+const nsIPropertyElement = Ci.nsIPropertyElement;
+
+const {
+  EVENT_TEXT_CHANGED,
+  EVENT_TEXT_INSERTED,
+  EVENT_TEXT_REMOVED,
+  EVENT_ACCELERATOR_CHANGE,
+  EVENT_ACTION_CHANGE,
+  EVENT_DEFACTION_CHANGE,
+  EVENT_DESCRIPTION_CHANGE,
+  EVENT_DOCUMENT_ATTRIBUTES_CHANGED,
+  EVENT_HELP_CHANGE,
+  EVENT_HIDE,
+  EVENT_NAME_CHANGE,
+  EVENT_OBJECT_ATTRIBUTE_CHANGED,
+  EVENT_REORDER,
+  EVENT_STATE_CHANGE,
+  EVENT_TEXT_ATTRIBUTE_CHANGED,
+  EVENT_VALUE_CHANGE
+} = nsIAccessibleEvent;
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * Set of actors that expose accessibility tree information to the
+ * devtools protocol clients.
+ *
+ * The |Accessibility| actor is the main entry point. It is used to request
+ * an AccessibleWalker actor that caches the tree of Accessible actors.
+ *
+ * The |AccessibleWalker| actor is used to cache all seen Accessible actors as
+ * well as observe all relevant accesible events.
+ *
+ * The |Accessible| actor provides information about a particular accessible
+ * object, its properties, , attributes, states, relations, etc.
+ */
+
+/**
+ * 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;
+    this.rawAccessible = rawAccessible;
+
+    /**
+     * Indicates if the raw accessible is no longer alive.
+     *
+     * @return Boolean
+     */
+    Object.defineProperty(this, "isDefunct", {
+      get() {
+        let defunct = false;
+
+        try {
+          let extState = {};
+          this.rawAccessible.getState({}, extState);
+          // extState.value is a bitmask. We are applying bitwise AND to mask out
+          // irrelelvant states.
+          defunct = !!(extState.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT);
+        } catch (e) {
+          defunct = true;
+        }
+
+        if (defunct) {
+          delete this.isDefunct;
+          this.isDefunct = true;
+          return this.isDefunct;
+        }
+
+        return defunct;
+      },
+      configurable: true
+    });
+  },
+
+  /**
+   * Items returned by this actor should belong to the parent walker.
+   */
+  marshallPool() {
+    return this.walker;
+  },
+
+  destroy() {
+    Actor.prototype.destroy.call(this);
+    this.walker = null;
+    this.rawAccessible = null;
+  },
+
+  get role() {
+    if (this.isDefunct) {
+      return null;
+    }
+    return this.walker.a11yService.getStringRole(this.rawAccessible.role);
+  },
+
+  get name() {
+    if (this.isDefunct) {
+      return null;
+    }
+    return this.rawAccessible.name;
+  },
+
+  get value() {
+    if (this.isDefunct) {
+      return null;
+    }
+    return this.rawAccessible.value;
+  },
+
+  get description() {
+    if (this.isDefunct) {
+      return null;
+    }
+    return this.rawAccessible.description;
+  },
+
+  get help() {
+    if (this.isDefunct) {
+      return null;
+    }
+    return this.rawAccessible.help;
+  },
+
+  get keyboardShortcut() {
+    if (this.isDefunct) {
+      return null;
+    }
+    return this.rawAccessible.keyboardShortcut;
+  },
+
+  get childCount() {
+    if (this.isDefunct) {
+      return 0;
+    }
+    return this.rawAccessible.childCount;
+  },
+
+  get domNodeType() {
+    if (this.isDefunct) {
+      return 0;
+    }
+    return this.rawAccessible.DOMNode ? this.rawAccessible.DOMNode.nodeType : 0;
+  },
+
+  children() {
+    let children = [];
+    if (this.isDefunct) {
+      return children;
+    }
+
+    for (let child = this.rawAccessible.firstChild; child; child = child.nextSibling) {
+      children.push(this.walker.addRef(child));
+    }
+    return children;
+  },
+
+  getIndexInParent() {
+    if (this.isDefunct) {
+      return -1;
+    }
+    return this.rawAccessible.indexInParent;
+  },
+
+  getActions() {
+    let actions = [];
+    if (this.isDefunct) {
+      return actions;
+    }
+
+    for (let i = 0; i < this.rawAccessible.actionCount; i++) {
+      actions.push(this.rawAccessible.getActionDescription(i));
+    }
+    return actions;
+  },
+
+  getState() {
+    if (this.isDefunct) {
+      return [];
+    }
+
+    let state = {};
+    let extState = {};
+    this.rawAccessible.getState(state, extState);
+    return [
+      ...this.walker.a11yService.getStringStates(state.value, extState.value)
+    ];
+  },
+
+  getAttributes() {
+    if (this.isDefunct || !this.rawAccessible.attributes) {
+      return {};
+    }
+
+    let attributes = {};
+    let attrsEnum = this.rawAccessible.attributes.enumerate();
+    while (attrsEnum.hasMoreElements()) {
+      let { key, value } = attrsEnum.getNext().QueryInterface(
+        nsIPropertyElement);
+      attributes[key] = value;
+    }
+
+    return attributes;
+  },
+
+  form() {
+    return {
+      actor: this.actorID,
+      role: this.role,
+      name: this.name,
+      value: this.value,
+      description: this.description,
+      help: this.help,
+      keyboardShortcut: this.keyboardShortcut,
+      childCount: this.childCount,
+      domNodeType: this.domNodeType,
+      walker: this.walker.form()
+    };
+  }
+});
+
+/**
+ * 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, {
+  initialize(conn, tabActor) {
+    Actor.prototype.initialize.call(this, conn);
+    this.tabActor = tabActor;
+    this.rootWin = tabActor.window;
+    this.rootDoc = tabActor.window.document;
+    this.refMap = new Map();
+    // Accessibility Walker should only be considered ready, when raw accessible
+    // object for root document is fully initialized (e.g. does not have a
+    // 'busy' state)
+    this.readyDeferred = defer();
+
+    DevToolsUtils.defineLazyGetter(this, "a11yService", () => {
+      Services.obs.addObserver(this, "accessible-event");
+      return Cc["@mozilla.org/accessibilityService;1"].getService(
+        Ci.nsIAccessibilityService);
+    });
+
+    this.onLoad = this.onLoad.bind(this);
+    this.onUnload = this.onUnload.bind(this);
+
+    events.on(tabActor, "will-navigate", this.onUnload);
+    events.on(tabActor, "window-ready", this.onLoad);
+  },
+
+  onUnload({ window }) {
+    let doc = window.document;
+    let actor = this.getRef(doc);
+
+    // If an accessible actor was never created for document, then there's
+    // nothing to clean up.
+    if (!actor) {
+      return;
+    }
+
+    // Purge document's subtree from accessible actors cache.
+    this.purgeSubtree(this.a11yService.getAccessibleFor(this.doc));
+    // If document is a root document, clear it's reference and cache.
+    if (this.rootDoc === doc) {
+      this.rootDoc = null;
+      this.refMap.clear();
+      this.readyDeferred = defer();
+    }
+  },
+
+  onLoad({ window, isTopLevel }) {
+    if (isTopLevel) {
+      // If root document is dead, unload it and clean up.
+      if (this.rootDoc && !Cu.isDeadWrapper(this.rootDoc) &&
+          this.rootDoc.defaultView) {
+        this.onUnload({ window: this.rootDoc.defaultView });
+      }
+
+      this.rootWin = window;
+      this.rootDoc = window.document;
+    }
+  },
+
+  destroy() {
+    if (this._destroyed) {
+      return;
+    }
+
+    this._destroyed = true;
+
+    try {
+      Services.obs.removeObserver(this, "accessible-event");
+    } catch (e) {
+      // Accessible event observer might not have been initialized if a11y
+      // service was never used.
+    }
+
+    // Clean up accessible actors cache.
+    if (this.refMap.size > 0) {
+      this.purgeSubtree(this.a11yService.getAccessibleFor(this.rootDoc));
+      this.refMap.clear();
+    }
+
+    events.off(this.tabActor, "will-navigate", this.onUnload);
+    events.off(this.tabActor, "window-ready", this.onLoad);
+
+    this.onLoad = null;
+    this.onUnload = null;
+    delete this.a11yService;
+    this.tabActor = null;
+    this.rootDoc = null;
+    this.refMap = null;
+
+    Actor.prototype.destroy.call(this);
+  },
+
+  getRef(rawAccessible) {
+    return this.refMap.get(rawAccessible);
+  },
+
+  addRef(rawAccessible) {
+    let actor = this.refMap.get(rawAccessible);
+    if (actor) {
+      return actor;
+    }
+
+    actor = new AccessibleActor(this, rawAccessible);
+    this.manage(actor);
+    this.refMap.set(rawAccessible, actor);
+
+    return actor;
+  },
+
+  /**
+   * Clean up accessible actors cache for a given accessible's subtree.
+   *
+   * @param  {nsIAccessible} rawAccessible
+   */
+  purgeSubtree(rawAccessible) {
+    let actor = this.getRef(rawAccessible);
+    if (actor && rawAccessible && !actor.isDefunct) {
+      for (let child = rawAccessible.firstChild; child; child = child.nextSibling) {
+        this.purgeSubtree(child);
+      }
+    }
+
+    this.refMap.delete(rawAccessible);
+
+    if (actor) {
+      events.emit(this, "accessible-destroy", actor);
+      actor.destroy();
+    }
+  },
+
+  /**
+   * A helper method. Accessibility walker is assumed to have only 1 child which
+   * is the top level document.
+   */
+  children() {
+    return Promise.all([this.getDocument()]);
+  },
+
+  /**
+   * A promise for a root document accessible actor that only resolves when its
+   * corresponding document accessible object is fully loaded.
+   *
+   * @return {Promise}
+   */
+  getDocument() {
+    let doc = this.addRef(this.a11yService.getAccessibleFor(this.rootDoc));
+    let states = doc.getState();
+
+    if (states.includes("busy")) {
+      return this.readyDeferred.promise.then(() => doc);
+    }
+
+    this.readyDeferred.resolve();
+    return Promise.resolve(doc);
+  },
+
+  getAccessibleFor(domNode) {
+    // We need to make sure that the document is loaded processed by a11y first.
+    return this.getDocument().then(() =>
+      this.addRef(this.a11yService.getAccessibleFor(domNode.rawNode)));
+  },
+
+  /**
+   * Accessible event observer function.
+   *
+   * @param {nsIAccessibleEvent} subject
+   *                                      accessible event object.
+   */
+  observe(subject) {
+    let event = subject.QueryInterface(nsIAccessibleEvent);
+    let rawAccessible = event.accessible;
+    let accessible = this.getRef(rawAccessible);
+
+    switch (event.eventType) {
+      case EVENT_STATE_CHANGE:
+        let { state, isEnabled } = event.QueryInterface(nsIAccessibleStateChangeEvent);
+        let states = [...this.a11yService.getStringStates(state, 0)];
+
+        if (states.includes("busy") && !isEnabled) {
+          let { DOMNode } = event;
+          // If debugging chrome, wait for top level content document loaded,
+          // otherwise wait for root document loaded.
+          if (DOMNode == this.rootDoc || (
+            this.rootDoc.documentElement.namespaceURI === XUL_NS &&
+            this.rootWin.gBrowser.selectedBrowser.contentDocument == DOMNode)) {
+            this.readyDeferred.resolve();
+          }
+        }
+
+        if (accessible) {
+          // Only propagate state change events for active accessibles.
+          if (states.includes("busy") && isEnabled) {
+            return;
+          }
+          events.emit(accessible, "state-change", accessible.getState());
+        }
+
+        break;
+      case EVENT_NAME_CHANGE:
+        if (accessible) {
+          events.emit(accessible, "name-change", rawAccessible.name,
+            event.DOMNode == this.rootDoc ?
+              undefined : this.getRef(rawAccessible.parent));
+        }
+        break;
+      case EVENT_VALUE_CHANGE:
+        if (accessible) {
+          events.emit(accessible, "value-change", rawAccessible.value);
+        }
+        break;
+      case EVENT_DESCRIPTION_CHANGE:
+        if (accessible) {
+          events.emit(accessible, "description-change", rawAccessible.description);
+        }
+        break;
+      case EVENT_HELP_CHANGE:
+        if (accessible) {
+          events.emit(accessible, "help-change", rawAccessible.help);
+        }
+        break;
+      case EVENT_REORDER:
+        if (accessible) {
+          events.emit(accessible, "reorder", rawAccessible.childCount);
+        }
+        break;
+      case EVENT_HIDE:
+        this.purgeSubtree(rawAccessible);
+        break;
+      case EVENT_DEFACTION_CHANGE:
+      case EVENT_ACTION_CHANGE:
+        if (accessible) {
+          events.emit(accessible, "actions-change", accessible.getActions());
+        }
+        break;
+      case EVENT_TEXT_CHANGED:
+      case EVENT_TEXT_INSERTED:
+      case EVENT_TEXT_REMOVED:
+        if (accessible) {
+          events.emit(accessible, "text-change");
+        }
+        break;
+      case EVENT_DOCUMENT_ATTRIBUTES_CHANGED:
+      case EVENT_OBJECT_ATTRIBUTE_CHANGED:
+      case EVENT_TEXT_ATTRIBUTE_CHANGED:
+        if (accessible) {
+          events.emit(accessible, "attributes-change", accessible.getAttributes());
+        }
+        break;
+      case EVENT_ACCELERATOR_CHANGE:
+        if (accessible) {
+          events.emit(accessible, "shortcut-change", rawAccessible.keyboardShortcut);
+        }
+        break;
+      default:
+        break;
+    }
+  }
+});
+
+/**
+ * The AccessibilityActor is a top level container actor that initializes
+ * accessible walker and is the top-most point of interaction for accessibility
+ * tools UI.
+ */
+const AccessibilityActor = ActorClassWithSpec(accessibilitySpec, {
+  initialize(conn, tabActor) {
+    Actor.prototype.initialize.call(this, conn);
+    this.tabActor = tabActor;
+  },
+
+  getWalker() {
+    if (!this.walker) {
+      this.walker = new AccessibleWalkerActor(this.conn, this.tabActor);
+    }
+    return this.walker;
+  },
+
+  destroy() {
+    Actor.prototype.destroy.call(this);
+    this.walker.destroy();
+    this.walker = null;
+    this.tabActor = null;
+  }
+});
+
+exports.AccessibleActor = AccessibleActor;
+exports.AccessibleWalkerActor = AccessibleWalkerActor;
+exports.AccessibilityActor = AccessibilityActor;
--- a/devtools/server/actors/moz.build
+++ b/devtools/server/actors/moz.build
@@ -6,16 +6,17 @@
 
 DIRS += [
     'highlighters',
     'utils',
     'webconsole',
 ]
 
 DevToolsModules(
+    'accessibility.js',
     'actor-registry.js',
     'addon.js',
     'addons.js',
     'animation.js',
     'breakpoint.js',
     'call-watcher.js',
     'canvas.js',
     'child-process.js',
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -580,16 +580,21 @@ var DebuggerServer = {
       constructor: "EmulationActor",
       type: { tab: true }
     });
     this.registerModule("devtools/server/actors/webextension-inspected-window", {
       prefix: "webExtensionInspectedWindow",
       constructor: "WebExtensionInspectedWindowActor",
       type: { tab: true }
     });
+    this.registerModule("devtools/server/actors/accessibility", {
+      prefix: "accessibility",
+      constructor: "AccessibilityActor",
+      type: { tab: true }
+    });
   },
 
   /**
    * Passes a set of options to the BrowserAddonActors for the given ID.
    *
    * @param id string
    *        The ID of the add-on to pass the options to
    * @param options object
--- a/devtools/server/tests/browser/browser.ini
+++ b/devtools/server/tests/browser/browser.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
   animation.html
+  doc_accessibility.html
   doc_allocations.html
   doc_force_cc.html
   doc_force_gc.html
   doc_innerHTML.html
   doc_perf.html
   grid.html
   inspectedwindow-reload-target.sjs
   navigate-first.html
@@ -20,16 +21,20 @@ support-files =
   storage-updates.html
   storage-secured-iframe.html
   stylesheets-nested-iframes.html
   timeline-iframe-child.html
   timeline-iframe-parent.html
   storage-helpers.js
   !/devtools/server/tests/mochitest/hello-actor.js
 
+[browser_accessibility_node_events.js]
+[browser_accessibility_node.js]
+[browser_accessibility_simple.js]
+[browser_accessibility_walker.js]
 [browser_animation_emitMutations.js]
 [browser_animation_getFrames.js]
 [browser_animation_getProperties.js]
 [browser_animation_getMultipleStates.js]
 [browser_animation_getPlayers.js]
 [browser_animation_getStateAfterFinished.js]
 [browser_animation_getSubTreeAnimations.js]
 [browser_animation_keepFinished.js]
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_node.js
@@ -0,0 +1,74 @@
+/* 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 AccessibleActor
+
+add_task(function* () {
+  let {client, walker, accessibility} =
+    yield initAccessibilityFrontForUrl(MAIN_DOMAIN + "doc_accessibility.html");
+
+  let a11yWalker = yield accessibility.getWalker(walker);
+  let buttonNode = yield walker.querySelector(walker.rootNode, "#button");
+  let accessibleFront = yield a11yWalker.getAccessibleFor(buttonNode);
+
+  checkA11yFront(accessibleFront, {
+    name: "Accessible Button",
+    role: "pushbutton",
+    value: "",
+    description: "Accessibility Test",
+    help: "",
+    keyboardShortcut: "",
+    childCount: 1,
+    domNodeType: 1
+  });
+
+  info("Actions");
+  let actions = yield accessibleFront.getActions();
+  is(actions.length, 1, "Accessible Front has correct number of actions");
+  is(actions[0], "Press", "Accessible Front default action is correct");
+
+  info("Index in parent");
+  let index = yield accessibleFront.getIndexInParent();
+  is(index, 1, "Accessible Front has correct index in parent");
+
+  info("State");
+  let state = yield accessibleFront.getState();
+  SimpleTest.isDeeply(state,
+    ["focusable", "selectable text", "opaque", "enabled", "sensitive"],
+    "Accessible Front has correct states");
+
+  info("Attributes");
+  let attributes = yield accessibleFront.getAttributes();
+  SimpleTest.isDeeply(attributes, {
+    "margin-top": "0px",
+    display: "inline-block",
+    "text-align": "center",
+    "text-indent": "0px",
+    "margin-left": "0px",
+    tag: "button",
+    "margin-right": "0px",
+    id: "button",
+    "margin-bottom": "0px"
+  }, "Accessible Front has correct attributes");
+
+  info("Children");
+  let children = yield accessibleFront.children();
+  is(children.length, 1, "Accessible Front has correct number of children");
+  checkA11yFront(children[0], {
+    name: "Accessible Button",
+    role: "text leaf"
+  });
+
+  info("DOM Node");
+  let node = yield accessibleFront.getDOMNode(walker);
+  is(node, buttonNode, "Accessible Front has correct DOM node");
+
+  let a11yShutdown = waitForA11yShutdown();
+  yield client.close();
+  forceCollections();
+  yield a11yShutdown;
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_node_events.js
@@ -0,0 +1,93 @@
+/* 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 AccessibleActor events
+
+add_task(function* () {
+  let {client, walker, accessibility} =
+    yield initAccessibilityFrontForUrl(MAIN_DOMAIN + "doc_accessibility.html");
+
+  let a11yWalker = yield accessibility.getWalker(walker);
+  let a11yDoc = yield a11yWalker.getDocument();
+  let buttonNode = yield walker.querySelector(walker.rootNode, "#button");
+  let accessibleFront = yield a11yWalker.getAccessibleFor(buttonNode);
+  let sliderNode = yield walker.querySelector(walker.rootNode, "#slider");
+  let accessibleSliderFront = yield a11yWalker.getAccessibleFor(sliderNode);
+  let browser = gBrowser.selectedBrowser;
+
+  checkA11yFront(accessibleFront, {
+    name: "Accessible Button",
+    role: "pushbutton",
+    value: "",
+    description: "Accessibility Test",
+    help: "",
+    keyboardShortcut: "",
+    childCount: 1,
+    domNodeType: 1
+  });
+
+  info("Name change event");
+  yield emitA11yEvent(accessibleFront, "name-change",
+    (name, parent) => {
+      checkA11yFront(accessibleFront, { name: "Renamed" });
+      checkA11yFront(parent, { }, a11yDoc);
+    }, () => ContentTask.spawn(browser, null, () =>
+      content.document.getElementById("button").setAttribute(
+        "aria-label", "Renamed")));
+
+  info("Description change event");
+  yield emitA11yEvent(accessibleFront, "description-change",
+    () => checkA11yFront(accessibleFront, { description: "" }),
+    () => ContentTask.spawn(browser, null, () =>
+      content.document.getElementById("button").removeAttribute("aria-describedby")));
+
+  info("State change event");
+  let states = yield accessibleFront.getState();
+  let expectedStates = ["unavailable", "selectable text", "opaque"];
+  SimpleTest.isDeeply(states, ["focusable", "selectable text", "opaque",
+                               "enabled", "sensitive"], "States are correct");
+  yield emitA11yEvent(accessibleFront, "state-change",
+    newStates => SimpleTest.isDeeply(newStates, expectedStates,
+                                     "States are updated"),
+    () => ContentTask.spawn(browser, null, () =>
+      content.document.getElementById("button").setAttribute("disabled", true)));
+  states = yield accessibleFront.getState();
+  SimpleTest.isDeeply(states, expectedStates, "States are updated");
+
+  info("Attributes change event");
+  let attrs = yield accessibleFront.getAttributes();
+  ok(!attrs.live, "Attribute is not present");
+  yield emitA11yEvent(accessibleFront, "attributes-change",
+    newAttrs => is(newAttrs.live, "polite", "Attributes are updated"),
+    () => ContentTask.spawn(browser, null, () =>
+      content.document.getElementById("button").setAttribute("aria-live", "polite")));
+  attrs = yield accessibleFront.getAttributes();
+  is(attrs.live, "polite", "Attributes are updated");
+
+  info("Value change event");
+  checkA11yFront(accessibleSliderFront, { value: "5" });
+  yield 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");
+  yield emitA11yEvent(accessibleSliderFront, "reorder",
+    childCount => is(childCount, 2, "Child count is updated"),
+    () => ContentTask.spawn(browser, null, () => {
+      let button = content.document.createElement("button");
+      button.innerText = "Slider button";
+      content.document.getElementById("slider").appendChild(button);
+    }));
+  is(accessibleSliderFront.childCount, 2, "Child count is updated");
+
+  let a11yShutdown = waitForA11yShutdown();
+  yield client.close();
+  forceCollections();
+  yield a11yShutdown;
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_simple.js
@@ -0,0 +1,21 @@
+/* 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";
+
+// Simple checks for the AccessibilityActor and AccessibleWalkerActor
+
+add_task(function* () {
+  let {client, accessibility} = yield initAccessibilityFrontForUrl(
+    "data:text/html;charset=utf-8,<title>test</title><div></div>");
+
+  ok(accessibility, "The AccessibilityFront was created");
+  ok(accessibility.getWalker, "The getWalker method exists");
+
+  let a11yWalker = yield accessibility.getWalker();
+  ok(a11yWalker, "The AccessibleWalkerFront was returned");
+
+  yield client.close();
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_accessibility_walker.js
@@ -0,0 +1,75 @@
+/* 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
+
+add_task(function* () {
+  let {client, walker, accessibility} =
+    yield initAccessibilityFrontForUrl(MAIN_DOMAIN + "doc_accessibility.html");
+
+  let a11yWalker = yield accessibility.getWalker(walker);
+  ok(a11yWalker, "The AccessibleWalkerFront was returned");
+
+  let a11yDoc = yield a11yWalker.getDocument();
+  ok(a11yDoc, "The AccessibleFront for root doc is created");
+
+  let children = yield a11yWalker.children();
+  is(children.length, 1,
+    "AccessibleWalker only has 1 child - root doc accessible");
+  is(a11yDoc, children[0],
+    "Root accessible must be AccessibleWalker's only child");
+
+  let buttonNode = yield walker.querySelector(walker.rootNode, "#button");
+  let accessibleFront = yield a11yWalker.getAccessibleFor(buttonNode);
+
+  checkA11yFront(accessibleFront, {
+    name: "Accessible Button",
+    role: "pushbutton"
+  });
+
+  let browser = gBrowser.selectedBrowser;
+
+  // Ensure name-change event is emitted by walker when cached accessible's name
+  // gets updated (via DOM manipularion).
+  yield emitA11yEvent(a11yWalker, "name-change",
+    (front, parent) => {
+      checkA11yFront(front, { name: "Renamed" }, accessibleFront);
+      checkA11yFront(parent, { }, a11yDoc);
+    },
+    () => ContentTask.spawn(browser, null, () =>
+      content.document.getElementById("button").setAttribute(
+      "aria-label", "Renamed")));
+
+  // Ensure reorder event is emitted by walker when DOM tree changes.
+  let docChildren = yield a11yDoc.children();
+  is(docChildren.length, 3, "Root doc should have correct number of children");
+
+  yield emitA11yEvent(a11yWalker, "reorder",
+    front => checkA11yFront(front, { }, a11yDoc),
+    () => ContentTask.spawn(browser, null, () => {
+      let input = content.document.createElement("input");
+      input.type = "text";
+      input.title = "This is a tooltip";
+      input.value = "New input";
+      content.document.body.appendChild(input);
+    }));
+
+  docChildren = yield a11yDoc.children();
+  is(docChildren.length, 4, "Root doc should have correct number of children");
+
+  // Ensure destory event is emitted by walker when cached accessible's raw
+  // accessible gets destroyed.
+  yield emitA11yEvent(a11yWalker, "accessible-destroy",
+    destroyedFront => checkA11yFront(destroyedFront, { }, accessibleFront),
+    () => ContentTask.spawn(browser, null, () =>
+      content.document.getElementById("button").remove()));
+
+  let a11yShutdown = waitForA11yShutdown();
+  yield client.close();
+  forceCollections();
+  yield a11yShutdown;
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/doc_accessibility.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf-8">
+  </head>
+<body>
+  <h1 id="h1">Accessibility Test</h1>
+  <button id="button" aria-describedby="h1">Accessible Button</button>
+  <div id="slider" role="slider" aria-valuenow="5"
+       aria-valuemin="0" aria-valuemax="7">slider</div>
+</body>
+</html>
--- a/devtools/server/tests/browser/head.js
+++ b/devtools/server/tests/browser/head.js
@@ -75,16 +75,32 @@ function* initLayoutFrontForUrl(url) {
   let form = yield connectDebuggerClient(client);
   let inspector = InspectorFront(client, form);
   let walker = yield inspector.getWalker();
   let layout = yield walker.getLayoutInspector();
 
   return {inspector, walker, layout, client};
 }
 
+function* initAccessibilityFrontForUrl(url) {
+  const {AccessibilityFront} = require("devtools/shared/fronts/accessibility");
+  const {InspectorFront} = require("devtools/shared/fronts/inspector");
+
+  yield addTab(url);
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  let form = yield connectDebuggerClient(client);
+  let inspector = InspectorFront(client, form);
+  let walker = yield inspector.getWalker();
+  let accessibility = AccessibilityFront(client, form);
+
+  return {inspector, walker, accessibility, client};
+}
+
 function initDebuggerServer() {
   try {
     // Sometimes debugger server does not get destroyed correctly by previous
     // tests.
     DebuggerServer.destroy();
   } catch (e) {
     info(`DebuggerServer destroy error: ${e}\n${e.stack}`);
   }
@@ -231,8 +247,58 @@ function waitForMarkerType(front, types,
   front.on(eventName, handler);
 
   return promise;
 }
 
 function getCookieId(name, domain, path) {
   return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`;
 }
+
+/**
+ * Trigger DOM activity and wait for the corresponding accessibility event.
+ * @param  {Object} emitter   Devtools event emitter, usually a front.
+ * @param  {Sting} name       Accessibility event in question.
+ * @param  {Function} handler Accessibility event handler function with checks.
+ * @param  {Promise} task     A promise that resolves when DOM activity is done.
+ */
+async function emitA11yEvent(emitter, name, handler, task) {
+  let promise = emitter.once(name, handler);
+  await task();
+  await promise;
+}
+
+/**
+ * Check that accessibilty front is correct and its attributes are also
+ * up-to-date.
+ * @param  {Object} front         Accessibility front to be tested.
+ * @param  {Object} expected      A map of a11y front properties to be verified.
+ * @param  {Object} expectedFront Expected accessibility front.
+ */
+function checkA11yFront(front, expected, expectedFront) {
+  ok(front, "The accessibility front is created");
+
+  if (expectedFront) {
+    is(front, expectedFront, "Matching accessibility front");
+  }
+
+  for (let key in expected) {
+    is(front[key], expected[key], `accessibility front has correct ${key}`);
+  }
+}
+
+/**
+ * Wait for accessibility service to shut down. We consider it shut down when
+ * an "a11y-init-or-shutdown" event is received with a value of "0".
+ */
+async function waitForA11yShutdown() {
+  await ContentTask.spawn(gBrowser.selectedBrowser, {}, () =>
+    new Promise(resolve => {
+      let observe = (subject, topic, data) => {
+        Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+
+        if (data === "0") {
+          resolve();
+        }
+      };
+      Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+    }));
+}
new file mode 100644
--- /dev/null
+++ b/devtools/shared/fronts/accessibility.js
@@ -0,0 +1,136 @@
+/* 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 DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const {
+  Front,
+  FrontClassWithSpec,
+  preEvent,
+  types
+} = require("devtools/shared/protocol.js");
+const {
+  accessibleSpec,
+  accessibleWalkerSpec,
+  accessibilitySpec
+} = require("devtools/shared/specs/accessibility");
+
+const events = require("devtools/shared/event-emitter");
+const ACCESSIBLE_PROPERTIES = [
+  "role",
+  "name",
+  "value",
+  "description",
+  "help",
+  "keyboardShortcut",
+  "childCount",
+  "domNodeType"
+];
+
+const AccessibleFront = FrontClassWithSpec(accessibleSpec, {
+  initialize(client, form) {
+    Front.prototype.initialize.call(this, client, form);
+
+    // Define getters for accesible properties that are received from the actor.
+    // Note: we would like accessible properties to be iterable for a11y
+    // clients.
+    for (let key of ACCESSIBLE_PROPERTIES) {
+      Object.defineProperty(this, key, {
+        get() {
+          return this._form[key];
+        },
+        enumerable: true
+      });
+    }
+  },
+
+  marshallPool() {
+    return this.walker;
+  },
+
+  form(form, detail) {
+    if (detail === "actorid") {
+      this.actorID = form;
+      return;
+    }
+
+    this.actorID = form.actor;
+    this._form = form;
+    DevToolsUtils.defineLazyGetter(this, "walker", () =>
+      types.getType("accessiblewalker").read(this._form.walker, this));
+  },
+
+  /**
+   * Get a dom node front from accessible actor's raw accessible object's
+   * DONNode property.
+   */
+  getDOMNode(domWalker) {
+    return domWalker.getNodeFromActor(this.actorID,
+                                      ["rawAccessible", "DOMNode"]);
+  },
+
+  nameChange: preEvent("name-change", function (name, parent) {
+    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 (this.walker) {
+      events.emit(this.walker, "name-change", this, parent);
+    }
+  }),
+
+  valueChange: preEvent("value-change", function (value) {
+    this._form.value = value;
+  }),
+
+  descriptionChange: preEvent("description-change", function (description) {
+    this._form.description = description;
+  }),
+
+  helpChange: preEvent("help-change", function (help) {
+    this._form.help = help;
+  }),
+
+  shortcutChange: preEvent("shortcut-change", function (keyboardShortcut) {
+    this._form.keyboardShortcut = keyboardShortcut;
+  }),
+
+  reorder: preEvent("reorder", function (childCount) {
+    this._form.childCount = childCount;
+    // Reorder event affects the tree rendering, we fire this event on
+    // accessibility walker as the point of interaction for UI.
+    if (this.walker) {
+      events.emit(this.walker, "reorder", this);
+    }
+  }),
+
+  textChange: preEvent("text-change", function () {
+    // Text event affects the tree rendering, we fire this event on
+    // accessibility walker as the point of interaction for UI.
+    if (this.walker) {
+      events.emit(this.walker, "text-change", this);
+    }
+  })
+});
+
+const AccessibleWalkerFront = FrontClassWithSpec(accessibleWalkerSpec, {
+  accessibleDestroy: preEvent("accessible-destroy", function (accessible) {
+    accessible.destroy();
+  }),
+
+  form(json) {
+    this.actorID = json.actor;
+  }
+});
+
+const AccessibilityFront = FrontClassWithSpec(accessibilitySpec, {
+  initialize(client, form) {
+    Front.prototype.initialize.call(this, client, form);
+    this.actorID = form.accessibilityActor;
+    this.manage(this);
+  }
+});
+
+exports.AccessibleFront = AccessibleFront;
+exports.AccessibleWalkerFront = AccessibleWalkerFront;
+exports.AccessibilityFront = AccessibilityFront;
--- a/devtools/shared/fronts/moz.build
+++ b/devtools/shared/fronts/moz.build
@@ -1,15 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
+    'accessibility.js',
     'actor-registry.js',
     'addons.js',
     'animation.js',
     'call-watcher.js',
     'canvas.js',
     'css-properties.js',
     'csscoverage.js',
     'device.js',
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/accessibility.js
@@ -0,0 +1,141 @@
+/* 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 protocol = require("devtools/shared/protocol");
+const { Arg, generateActorSpec, RetVal, types } = protocol;
+// eslint-disable-next-line no-unused-vars
+const { nodeSpec } = require("devtools/shared/specs/inspector");
+
+types.addActorType("accessible");
+
+const accessibleSpec = generateActorSpec({
+  typeName: "accessible",
+
+  events: {
+    "actions-change": {
+      type: "actionsChange",
+      actions: Arg(0, "array:string")
+    },
+    "name-change": {
+      type: "nameChange",
+      name: Arg(0, "string"),
+      parent: Arg(1, "nullable:accessible")
+    },
+    "value-change": {
+      type: "valueChange",
+      value: Arg(0, "string")
+    },
+    "description-change": {
+      type: "descriptionChange",
+      description: Arg(0, "string")
+    },
+    "state-change": {
+      type: "stateChange",
+      states: Arg(0, "array:string")
+    },
+    "attributes-change": {
+      type: "attributesChange",
+      states: Arg(0, "json")
+    },
+    "help-change": {
+      type: "helpChange",
+      help: Arg(0, "string")
+    },
+    "shortcut-change": {
+      type: "shortcutChange",
+      shortcut: Arg(0, "string")
+    },
+    "reorder": {
+      type: "reorder",
+      childCount: Arg(0, "number")
+    },
+    "text-change": {
+      type: "textChange"
+    }
+  },
+
+  methods: {
+    getActions: {
+      request: {},
+      response: {
+        actions: RetVal("array:string")
+      }
+    },
+    getIndexInParent: {
+      request: {},
+      response: {
+        indexInParent: RetVal("number")
+      }
+    },
+    getState: {
+      request: {},
+      response: {
+        states: RetVal("array:string")
+      }
+    },
+    getAttributes: {
+      request: {},
+      response: {
+        attributes: RetVal("json")
+      }
+    },
+    children: {
+      request: {},
+      response: {
+        children: RetVal("array:accessible")
+      }
+    }
+  }
+});
+
+const accessibleWalkerSpec = generateActorSpec({
+  typeName: "accessiblewalker",
+
+  events: {
+    "accessible-destroy": {
+      type: "accessibleDestroy",
+      accessible: Arg(0, "accessible")
+    }
+  },
+
+  methods: {
+    children: {
+      request: {},
+      response: {
+        children: RetVal("array:accessible")
+      }
+    },
+    getDocument: {
+      request: {},
+      response: {
+        document: RetVal("accessible")
+      }
+    },
+    getAccessibleFor: {
+      request: { node: Arg(0, "domnode") },
+      response: {
+        accessible: RetVal("accessible")
+      }
+    }
+  }
+});
+
+const accessibilitySpec = generateActorSpec({
+  typeName: "accessibility",
+
+  methods: {
+    getWalker: {
+      request: {},
+      response: {
+        walker: RetVal("accessiblewalker")
+      }
+    }
+  }
+});
+
+exports.accessibleSpec = accessibleSpec;
+exports.accessibleWalkerSpec = accessibleWalkerSpec;
+exports.accessibilitySpec = accessibilitySpec;
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -1,15 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
+    'accessibility.js',
     'actor-registry.js',
     'addons.js',
     'animation.js',
     'breakpoint.js',
     'call-watcher.js',
     'canvas.js',
     'css-properties.js',
     'csscoverage.js',