Bug 1428441 - adding 'Show Accessibility Properties' context menu item for inspector markup view nodes. r=pbro
authorYura Zenevich <yura.zenevich@gmail.com>
Wed, 04 Apr 2018 13:16:18 -0400
changeset 412427 86b6ee084e0c7f03d969a39b8ed95ffd97b1bc15
parent 412426 ed4586ca3284de25efbe1bdcd6d481a4d8b8688c
child 412428 2af150038b3befc44e4cc07f1aab73119f8f7c3d
push id33804
push userapavel@mozilla.com
push dateMon, 09 Apr 2018 21:56:10 +0000
treeherdermozilla-central@83de58ddda20 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1428441
milestone61.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 1428441 - adding 'Show Accessibility Properties' context menu item for inspector markup view nodes. r=pbro MozReview-Commit-ID: EEE6VaCgIza
devtools/client/accessibility/accessibility-panel.js
devtools/client/accessibility/test/browser.ini
devtools/client/accessibility/test/browser_accessibility_context_menu_inspector.js
devtools/client/accessibility/test/head.js
devtools/client/inspector/inspector.js
devtools/client/inspector/test/head.js
devtools/client/inspector/test/shared-head.js
devtools/client/locales/en-US/inspector.properties
devtools/server/actors/inspector/walker.js
devtools/shared/specs/inspector.js
--- a/devtools/client/accessibility/accessibility-panel.js
+++ b/devtools/client/accessibility/accessibility-panel.js
@@ -136,16 +136,20 @@ AccessibilityPanel.prototype = {
     this.shouldRefresh = false;
     this.postContentMessage("initialize", this._front, this._walker, this._isOldVersion);
   },
 
   selectAccessible(accessibleFront) {
     this.postContentMessage("selectAccessible", this._walker, accessibleFront);
   },
 
+  selectAccessibleForNode(nodeFront) {
+    this.postContentMessage("selectNodeAccessible", this._walker, nodeFront);
+  },
+
   highlightAccessible(accessibleFront) {
     this.postContentMessage("highlightAccessible", this._walker, accessibleFront);
   },
 
   postContentMessage(type, ...args) {
     const event = new this.panelWin.MessageEvent("devtools/chrome/message", {
       bubbles: true,
       cancelable: true,
--- a/devtools/client/accessibility/test/browser.ini
+++ b/devtools/client/accessibility/test/browser.ini
@@ -2,13 +2,14 @@
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/shared-redux-head.js
 
+[browser_accessibility_context_menu_inspector.js]
 [browser_accessibility_mutations.js]
 [browser_accessibility_reload.js]
 [browser_accessibility_sidebar.js]
 [browser_accessibility_tree.js]
 [browser_accessibility_tree_nagivation.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/test/browser_accessibility_context_menu_inspector.js
@@ -0,0 +1,43 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_URI = "<h1 id=\"h1\">header</h1><p id=\"p\">paragraph</p>";
+
+addA11YPanelTask("Test show accessibility properties context menu.", TEST_URI,
+  async function testShowAccessibilityPropertiesContextMenu({ panel, toolbox }) {
+    let inspector = await toolbox.selectTool("inspector");
+
+    ok(inspector.selection.isBodyNode(), "Default selection is a body node.");
+    let menuUpdated = inspector.once("node-menu-updated");
+    let allMenuItems = openContextMenuAndGetAllItems(inspector);
+    let showA11YPropertiesNode = allMenuItems.find(item =>
+      item.id === "node-menu-showaccessibilityproperties");
+    ok(showA11YPropertiesNode,
+      "the popup menu now has a show accessibility properties item");
+    await menuUpdated;
+    ok(showA11YPropertiesNode.disabled, "Body node does not have accessible");
+
+    await selectNode("#h1", inspector, "test");
+    menuUpdated = inspector.once("node-menu-updated");
+    allMenuItems = openContextMenuAndGetAllItems(inspector);
+    showA11YPropertiesNode = allMenuItems.find(item =>
+      item.id === "node-menu-showaccessibilityproperties");
+    ok(showA11YPropertiesNode,
+      "the popup menu now has a show accessibility properties item");
+    await menuUpdated;
+    ok(!showA11YPropertiesNode.disabled, "Body node has an accessible");
+
+    info("Triggering 'Show Accessibility Properties' and waiting for " +
+         "accessibility panel to open");
+    let panelSelected = toolbox.once("accessibility-selected");
+    let objectSelected = panel.once("new-accessible-front-selected");
+    showA11YPropertiesNode.click();
+    await panelSelected;
+    let selected = await objectSelected;
+
+    let expectedSelected = await panel.walker.getAccessibleFor(
+      inspector.selection.nodeFront);
+    is(selected, expectedSelected, "Accessible front selected correctly");
+  });
--- a/devtools/client/accessibility/test/head.js
+++ b/devtools/client/accessibility/test/head.js
@@ -94,29 +94,34 @@ async function addTestTab(url) {
   // accessibility panel test can be too fast.
   await win.gToolbox.loadTool("inspector");
 
   return {
     tab,
     browser: tab.linkedBrowser,
     panel,
     win,
+    toolbox: panel._toolbox,
     doc,
     store
   };
 }
 
 /**
  * Turn off accessibility features from within the panel. We call it before the
  * cleanup function to make sure that the panel is still present.
  */
-function disableAccessibilityInspector(env) {
-  let { doc, win } = env;
+async function disableAccessibilityInspector(env) {
+  let { doc, win, panel } = env;
+  // Disable accessibility service through the panel and wait for the shutdown
+  // event.
+  let shutdown = panel._front.once("shutdown");
   EventUtils.sendMouseEvent({ type: "click" },
     doc.getElementById("accessibility-disable-button"), win);
+  await shutdown;
 }
 
 /**
  * Open the Accessibility panel for the given tab.
  *
  * @param {nsIDOMElement} tab
  *        Optional tab element for which you want open the Accessibility panel.
  *        The default tab is taken from the global variable |tab|.
@@ -237,17 +242,17 @@ async function runA11yPanelTests(tests, 
 }
 
 /**
  * Build a valid URL from an HTML snippet.
  * @param  {String} uri HTML snippet
  * @return {String}     built URL
  */
 function buildURL(uri) {
-  return `data:text/html,${encodeURIComponent(uri)}`;
+  return `data:text/html;charset=UTF-8,${encodeURIComponent(uri)}`;
 }
 
 /**
  * Add a test task based on the test structure and a test URL.
  * @param  {JSON}   tests  test data that has the format of:
  *                    {
  *                      desc     {String}    description for better logging
  *                      action   {Function}  An optional action that needs to be
@@ -255,25 +260,32 @@ function buildURL(uri) {
  *                                           tree and the sidebar can be checked
  *                      expected {JSON}      An expected states for the tree and
  *                                           the sidebar
  *                    }
  * @param {String}  uri    test URL
  * @param {String}  msg    a message that is printed for the test
  */
 function addA11yPanelTestsTask(tests, uri, msg) {
-  tests.push({
-    desc: "Disable accessibility inspector.",
-    action: env => disableAccessibilityInspector(env),
-    expected: {}
-  });
-  add_task(async function a11yPanelTests() {
+  addA11YPanelTask(msg, uri, env => runA11yPanelTests(tests, env));
+}
+
+/**
+ * A wrapper function around add_task that sets up the test environment, runs
+ * the test and then disables accessibility tools.
+ * @param {String}   msg    a message that is printed for the test
+ * @param {String}   uri    test URL
+ * @param {Function} task   task function containing the tests.
+ */
+function addA11YPanelTask(msg, uri, task) {
+  add_task(async function a11YPanelTask() {
     info(msg);
     let env = await addTestTab(buildURL(uri));
-    await runA11yPanelTests(tests, env);
+    await task(env);
+    await disableAccessibilityInspector(env);
   });
 }
 
 /**
  * Reload panel target.
  * @param  {Object} target             Panel target.
  * @param  {String} waitForTargetEvent Event to wait for after reload.
  */
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -1561,32 +1561,66 @@ Inspector.prototype = {
       click: () => this.useInConsole(),
     }));
     menu.append(new MenuItem({
       id: "node-menu-showdomproperties",
       label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"),
       click: () => this.showDOMProperties(),
     }));
 
+    this.buildA11YMenuItem(menu);
+
     let nodeLinkMenuItems = this._getNodeLinkMenuItems();
     if (nodeLinkMenuItems.filter(item => item.visible).length > 0) {
       menu.append(new MenuItem({
         id: "node-menu-link-separator",
         type: "separator",
       }));
     }
 
     for (let menuitem of nodeLinkMenuItems) {
       menu.append(menuitem);
     }
 
     menu.popup(screenX, screenY, this._toolbox);
     return menu;
   },
 
+  buildA11YMenuItem: function(menu) {
+    if (!this.selection.isElementNode() ||
+        !Services.prefs.getBoolPref("devtools.accessibility.enabled")) {
+      return;
+    }
+
+    const showA11YPropsItem = new MenuItem({
+      id: "node-menu-showaccessibilityproperties",
+      label: INSPECTOR_L10N.getStr("inspectorShowAccessibilityProperties.label"),
+      click: () => this.showAccessibilityProperties(),
+      disabled: true
+    });
+    this._updateA11YMenuItem(showA11YPropsItem);
+    menu.append(showA11YPropsItem);
+  },
+
+  _updateA11YMenuItem: async function(menuItem) {
+    const hasMethod = await this.target.actorHasMethod("domwalker",
+                                                       "hasAccessibilityProperties");
+    if (!hasMethod) {
+      return;
+    }
+
+    const hasA11YProps = await this.walker.hasAccessibilityProperties(
+      this.selection.nodeFront);
+    if (hasA11YProps) {
+      this._toolbox.doc.getElementById(menuItem.id).disabled = menuItem.disabled = false;
+    }
+
+    this.emit("node-menu-updated");
+  },
+
   _getCopySubmenu: function(markupContainer, isSelectionElement) {
     let copySubmenu = new Menu();
     copySubmenu.append(new MenuItem({
       id: "node-menu-copyinner",
       label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"),
       accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"),
       disabled: !isSelectionElement,
       click: () => this.copyInnerHTML(),
@@ -1949,16 +1983,28 @@ Inspector.prototype = {
       let jsterm = panel.hud.jsterm;
 
       jsterm.execute("inspect($0)");
       jsterm.focus();
     });
   },
 
   /**
+   * Show Accessibility properties for currently selected node
+   */
+  async showAccessibilityProperties() {
+    let a11yPanel = await this._toolbox.selectTool("accessibility");
+    // Select the accessible object in the panel and wait for the event that
+    // tells us it has been done.
+    let onSelected = a11yPanel.once("new-accessible-front-selected");
+    a11yPanel.selectAccessibleForNode(this.selection.nodeFront);
+    await onSelected;
+  },
+
+  /**
    * Use in Console.
    *
    * Takes the currently selected node in the inspector and assigns it to a
    * temp variable on the content window.  Also opens the split console and
    * autofills it with the temp variable.
    */
   useInConsole: function() {
     this._toolbox.openSplitConsole().then(() => {
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -745,36 +745,16 @@ async function assertTooltipHiddenOnMous
   target.parentNode.dispatchEvent(mouseEvent);
 
   await tooltip.once("hidden");
 
   ok(!tooltip.isVisible(), "The tooltip is hidden on mouseout");
 }
 
 /**
- * Open the inspector menu and return all of it's items in a flat array
- * @param {InspectorPanel} inspector
- * @param {Object} options to pass into openMenu
- * @return An array of MenuItems
- */
-function openContextMenuAndGetAllItems(inspector, options) {
-  let menu = inspector._openMenu(options);
-
-  // Flatten all menu items into a single array to make searching through it easier
-  let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
-    if (item.submenu) {
-      return addItem(item.submenu.items);
-    }
-    return item;
-  }));
-
-  return allItems;
-}
-
-/**
  * Get the rule editor from the rule-view given its index
  *
  * @param {CssRuleView} view
  *        The instance of the rule-view panel
  * @param {Number} childrenIndex
  *        The children index of the element to get
  * @param {Number} nodeIndex
  *        The child node index of the element to get
--- a/devtools/client/inspector/test/shared-head.js
+++ b/devtools/client/inspector/test/shared-head.js
@@ -568,26 +568,43 @@ var setSearchFilter = async function(vie
   for (let key of searchValue.split("")) {
     EventUtils.synthesizeKey(key, {}, view.styleWindow);
   }
 
   await view.inspector.once("ruleview-filtered");
 };
 
 /**
- * Open the style editor context menu and return all of it's items in a flat array
- * @param {CssRuleView} view
- *        The instance of the rule-view panel
- * @return An array of MenuItems
+ * Flatten all context menu items into a single array to make searching through
+ * it easier.
  */
-function openStyleContextMenuAndGetAllItems(view, target) {
-  let menu = view._contextmenu._openMenu({target: target});
-
-  // Flatten all menu items into a single array to make searching through it easier
-  let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
+function buildContextMenuItems(menu) {
+  const allItems = [].concat.apply([], menu.items.map(function addItem(item) {
     if (item.submenu) {
       return addItem(item.submenu.items);
     }
     return item;
   }));
 
   return allItems;
 }
+
+/**
+ * Open the style editor context menu and return all of it's items in a flat array
+ * @param {CssRuleView} view
+ *        The instance of the rule-view panel
+ * @return An array of MenuItems
+ */
+function openStyleContextMenuAndGetAllItems(view, target) {
+  const menu = view._contextmenu._openMenu({target: target});
+  return buildContextMenuItems(menu);
+}
+
+/**
+ * Open the inspector menu and return all of it's items in a flat array
+ * @param {InspectorPanel} inspector
+ * @param {Object} options to pass into openMenu
+ * @return An array of MenuItems
+ */
+function openContextMenuAndGetAllItems(inspector, options) {
+  const menu = inspector._openMenu(options);
+  return buildContextMenuItems(menu);
+}
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -296,16 +296,23 @@ inspectorSearchHTML.label3=Search HTML
 inspectorImageDataUri.label=Image Data-URL
 
 # LOCALIZATION NOTE (inspectorShowDOMProperties.label): This is the label
 # shown in the inspector contextual-menu for the item that lets users see
 # the DOM properties of the current node. When triggered, this item
 # opens the split Console and displays the properties in its side panel.
 inspectorShowDOMProperties.label=Show DOM Properties
 
+# LOCALIZATION NOTE (inspectorShowAccessibilityProperties.label): This is the
+# label shown in the inspector contextual-menu for the item that lets users see
+# the accessibility tree and accessibility properties of the current node.
+# When triggered, this item opens accessibility panel and selects an accessible
+# object for the given node.
+inspectorShowAccessibilityProperties.label=Show Accessibility Properties
+
 # LOCALIZATION NOTE (inspectorUseInConsole.label): This is the label
 # shown in the inspector contextual-menu for the item that outputs a
 # variable for the current node to the console. When triggered,
 # this item opens the split Console.
 inspectorUseInConsole.label=Use in Console
 
 # LOCALIZATION NOTE (inspectorExpandNode.label): This is the label
 # shown in the inspector contextual-menu for recursively expanding
--- a/devtools/server/actors/inspector/walker.js
+++ b/devtools/server/actors/inspector/walker.js
@@ -1,15 +1,15 @@
 /* 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, Cu} = require("chrome");
+const {Cc, Ci, Cu} = require("chrome");
 
 const Services = require("Services");
 const protocol = require("devtools/shared/protocol");
 const {walkerSpec} = require("devtools/shared/specs/inspector");
 const {LongStringActor} = require("devtools/server/actors/string");
 const InspectorUtils = require("InspectorUtils");
 
 loader.lazyRequireGetter(this, "getFrameElement", "devtools/shared/layout/utils", true);
@@ -2009,11 +2009,26 @@ var WalkerActor = protocol.ActorClassWit
     let offsetParent = node.rawNode.offsetParent;
 
     if (!offsetParent) {
       return null;
     }
 
     return this._ref(offsetParent);
   },
+
+  /**
+   * Returns true if accessibility service is running and the node has a
+   * corresponding valid accessible object.
+   */
+  hasAccessibilityProperties: async function(node) {
+    if (isNodeDead(node) || !Services.appinfo.accessibilityEnabled) {
+      return false;
+    }
+
+    const accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+      Ci.nsIAccessibilityService);
+    const acc = accService.getAccessibleFor(node.rawNode);
+    return acc && acc.indexInParent > -1;
+  },
 });
 
 exports.WalkerActor = WalkerActor;
--- a/devtools/shared/specs/inspector.js
+++ b/devtools/shared/specs/inspector.js
@@ -325,16 +325,24 @@ const walkerSpec = generateActorSpec({
     getOffsetParent: {
       request: {
         node: Arg(0, "nullable:domnode")
       },
       response: {
         node: RetVal("nullable:domnode")
       }
     },
+    hasAccessibilityProperties: {
+      request: {
+        node: Arg(0, "nullable:domnode")
+      },
+      response: {
+        value: RetVal("boolean")
+      }
+    }
   }
 });
 
 exports.walkerSpec = walkerSpec;
 
 const inspectorSpec = generateActorSpec({
   typeName: "inspector",