Bug 1210778 - improving accessibility for about:debugging. r=janx draft
authorYura Zenevich <yzenevich@mozilla.com>
Thu, 21 Apr 2016 11:10:14 -0400
changeset 354842 e2c119411643030859164942d30f3e0c8af88299
parent 354827 1152d99d8c53ac9dae371a6e6d9fab03d3f98697
child 519082 dfe1b8d11bc4340fbe7e36f3eb328f6eb4889751
push id16165
push useryura.zenevich@gmail.com
push dateThu, 21 Apr 2016 15:10:38 +0000
reviewersjanx
bugs1210778
milestone48.0a1
Bug 1210778 - improving accessibility for about:debugging. r=janx MozReview-Commit-ID: 94il4DHDUOS --- devtools/.eslintrc.mochitests | 10 ++++++++ devtools/client/aboutdebugging/aboutdebugging.css | 5 ++++ .../aboutdebugging/components/aboutdebugging.js | 4 ++- .../aboutdebugging/components/addon-target.js | 2 +- .../client/aboutdebugging/components/addons-tab.js | 4 +-- .../aboutdebugging/components/tab-menu-entry.js | 11 +++++++- .../client/aboutdebugging/components/tab-menu.js | 8 +++--- .../aboutdebugging/components/target-list.js | 4 +-- .../aboutdebugging/components/worker-target.js | 2 +- .../aboutdebugging/components/workers-tab.js | 4 +-- .../aboutdebugging/test/browser_service_workers.js | 2 +- .../test/browser_service_workers_push.js | 2 +- .../test/browser_service_workers_start.js | 2 +- .../test/browser_service_workers_timeout.js | 2 +- .../test/browser_service_workers_unregister.js | 2 +- devtools/client/aboutdebugging/test/head.js | 30 +++++++++++++++++++--- 16 files changed, 72 insertions(+), 22 deletions(-)
devtools/.eslintrc.mochitests
devtools/client/aboutdebugging/aboutdebugging.css
devtools/client/aboutdebugging/components/aboutdebugging.js
devtools/client/aboutdebugging/components/addon-target.js
devtools/client/aboutdebugging/components/addons-tab.js
devtools/client/aboutdebugging/components/tab-menu-entry.js
devtools/client/aboutdebugging/components/tab-menu.js
devtools/client/aboutdebugging/components/target-list.js
devtools/client/aboutdebugging/components/worker-target.js
devtools/client/aboutdebugging/components/workers-tab.js
devtools/client/aboutdebugging/test/browser_service_workers.js
devtools/client/aboutdebugging/test/browser_service_workers_push.js
devtools/client/aboutdebugging/test/browser_service_workers_start.js
devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
devtools/client/aboutdebugging/test/browser_service_workers_unregister.js
devtools/client/aboutdebugging/test/head.js
--- a/devtools/.eslintrc.mochitests
+++ b/devtools/.eslintrc.mochitests
@@ -1,15 +1,25 @@
 // Parent config file for all devtools browser mochitest files.
 {
   "extends": [
     "../testing/mochitest/browser.eslintrc"
   ],
   // All globals made available in the test environment.
   "globals": {
+    "assertHasTarget": true,
+    "closeAboutDebugging": true,
     "DevToolsUtils": true,
     "gDevTools": true,
+    "getServiceWorkerList": true,
+    "installAddon": true,
     "once": true,
+    "openAboutDebugging": true,
     "synthesizeKeyFromKeyTag": true,
     "TargetFactory": true,
+    "uninstallAddon": true,
+    "unregisterServiceWorker": true,
+    "waitForInitialAddonList": true,
+    "waitForMutation": true,
+    "waitForServiceWorkerRegistered": true,
     "waitForTick": true,
   }
 }
--- a/devtools/client/aboutdebugging/aboutdebugging.css
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -45,16 +45,21 @@ button {
 }
 
 /* Targets */
 
 .targets {
   margin-bottom: 35px;
 }
 
+.target-list {
+  margin: 0;
+  padding: 0;
+}
+
 .target-container {
   margin-top: 5px;
   min-height: 34px;
   display: flex;
   flex-direction: row;
   align-items: baseline;
 }
 
--- a/devtools/client/aboutdebugging/components/aboutdebugging.js
+++ b/devtools/client/aboutdebugging/components/aboutdebugging.js
@@ -18,21 +18,23 @@ loader.lazyGetter(this, "AddonsTab",
 loader.lazyGetter(this, "WorkersTab",
   () => createFactory(require("./workers-tab")));
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 const tabs = [{
   id: "addons",
+  panelId: "tab-addons",
   name: Strings.GetStringFromName("addons"),
   icon: "chrome://devtools/skin/images/debugging-addons.svg",
   component: AddonsTab
 }, {
   id: "workers",
+  panelId: "tab-workers",
   name: Strings.GetStringFromName("workers"),
   icon: "chrome://devtools/skin/images/debugging-workers.svg",
   component: WorkersTab
 }];
 
 const defaultTabId = "addons";
 
 module.exports = createClass({
@@ -78,13 +80,13 @@ module.exports = createClass({
     let { selectedTabId } = this.state;
     let selectTab = this.selectTab;
 
     let selectedTab = tabs.find(t => t.id == selectedTabId);
 
     return dom.div({ className: "app" },
       TabMenu({ tabs, selectedTabId, selectTab }),
       dom.div({ className: "main-content" },
-        selectedTab.component({ client })
+        selectedTab.component({ client, id: selectedTab.panelId })
       )
     );
   }
 });
--- a/devtools/client/aboutdebugging/components/addon-target.js
+++ b/devtools/client/aboutdebugging/components/addon-target.js
@@ -36,17 +36,17 @@ module.exports = createClass({
       throw new Error(
         "Error reloading addon " + target.addonID + ": " + error);
     });
   },
 
   render() {
     let { target, debugDisabled } = this.props;
 
-    return dom.div({ className: "target-container" },
+    return dom.li({ className: "target-container" },
       dom.img({
         className: "target-icon",
         role: "presentation",
         src: target.icon
       }),
       dom.div({ className: "target" },
         dom.div({ className: "target-name" }, target.name)
       ),
--- a/devtools/client/aboutdebugging/components/addons-tab.js
+++ b/devtools/client/aboutdebugging/components/addons-tab.js
@@ -101,23 +101,23 @@ module.exports = createClass({
   /**
    * Mandatory callback as AddonManager listener.
    */
   onDisabled() {
     this.updateAddonsList();
   },
 
   render() {
-    let { client } = this.props;
+    let { client, id } = this.props;
     let { debugDisabled, extensions: targets } = this.state;
     let name = Strings.GetStringFromName("extensions");
     let targetClass = AddonTarget;
 
     return dom.div({
-      id: "tab-addons",
+      id: id,
       className: "tab",
       role: "tabpanel",
       "aria-labelledby": "tab-addons-header-name"
     },
     TabHeader({
       id: "tab-addons-header-name",
       name: Strings.GetStringFromName("addons")
     }),
--- a/devtools/client/aboutdebugging/components/tab-menu-entry.js
+++ b/devtools/client/aboutdebugging/components/tab-menu-entry.js
@@ -9,23 +9,32 @@ const { createClass, DOM: dom } =
 
 module.exports = createClass({
   displayName: "TabMenuEntry",
 
   onClick() {
     this.props.selectTab(this.props.tabId);
   },
 
+  onKeyUp(event) {
+    if ([" ", "Enter"].includes(event.key)) {
+      this.props.selectTab(this.props.tabId);
+    }
+  },
+
   render() {
-    let { icon, name, selected } = this.props;
+    let { panelId, icon, name, selected } = this.props;
 
     // Here .category, .category-icon, .category-name classnames are used to
     // apply common styles defined.
     let className = "category" + (selected ? " selected" : "");
     return dom.div({
       "aria-selected": selected,
+      "aria-controls": panelId,
       className,
       onClick: this.onClick,
+      onKeyUp: this.onKeyUp,
+      tabIndex: "0",
       role: "tab" },
     dom.img({ className: "category-icon", src: icon, role: "presentation" }),
     dom.div({ className: "category-name" }, name));
   }
 });
--- a/devtools/client/aboutdebugging/components/tab-menu.js
+++ b/devtools/client/aboutdebugging/components/tab-menu.js
@@ -8,17 +8,19 @@ const { createClass, createFactory, DOM:
   require("devtools/client/shared/vendor/react");
 const TabMenuEntry = createFactory(require("./tab-menu-entry"));
 
 module.exports = createClass({
   displayName: "TabMenu",
 
   render() {
     let { tabs, selectedTabId, selectTab } = this.props;
-    let tabLinks = tabs.map(({ id, name, icon }) => {
+    let tabLinks = tabs.map(({ panelId, id, name, icon }) => {
       let selected = id == selectedTabId;
-      return TabMenuEntry({ tabId: id, name, icon, selected, selectTab });
+      return TabMenuEntry({
+        tabId: id, panelId, name, icon, selected, selectTab
+      });
     });
 
     // "categories" id used for styling purposes
-    return dom.div({ id: "categories" }, tabLinks);
+    return dom.div({ id: "categories", role: "tablist" }, tabLinks);
   },
 });
--- a/devtools/client/aboutdebugging/components/target-list.js
+++ b/devtools/client/aboutdebugging/components/target-list.js
@@ -20,15 +20,15 @@ module.exports = createClass({
 
   render() {
     let { client, debugDisabled, targetClass } = this.props;
     let targets = this.props.targets.sort(LocaleCompare).map(target => {
       return targetClass({ client, target, debugDisabled });
     });
 
     return dom.div({ id: this.props.id, className: "targets" },
-      dom.h4(null, this.props.name),
+      dom.h2(null, this.props.name),
       targets.length > 0 ?
-        targets :
+        dom.ul({ className: "target-list" }, targets) :
         dom.p(null, Strings.GetStringFromName("nothing"))
     );
   },
 });
--- a/devtools/client/aboutdebugging/components/worker-target.js
+++ b/devtools/client/aboutdebugging/components/worker-target.js
@@ -20,17 +20,17 @@ module.exports = createClass({
   debug() {
     let { client, target } = this.props;
     debugWorker(client, target.workerActor);
   },
 
   render() {
     let { target, debugDisabled } = this.props;
 
-    return dom.div({ className: "target-container" },
+    return dom.li({ className: "target-container" },
       dom.img({
         className: "target-icon",
         role: "presentation",
         src: target.icon
       }),
       dom.div({ className: "target" },
         dom.div({ className: "target-name" }, target.name)
       ),
--- a/devtools/client/aboutdebugging/components/workers-tab.js
+++ b/devtools/client/aboutdebugging/components/workers-tab.js
@@ -95,21 +95,21 @@ module.exports = createClass({
       // find the scriptSpec.
       workers.service = workers.service.filter(reg => !!reg.url);
 
       this.setState({ workers });
     });
   },
 
   render() {
-    let { client } = this.props;
+    let { client, id } = this.props;
     let { workers } = this.state;
 
     return dom.div({
-      id: "tab-workers",
+      id: id,
       className: "tab",
       role: "tabpanel",
       "aria-labelledby": "tab-workers-header-name"
     },
     TabHeader({
       id: "tab-workers-header-name",
       name: Strings.GetStringFromName("workers")
     }),
--- a/devtools/client/aboutdebugging/test/browser_service_workers.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers.js
@@ -21,17 +21,17 @@ add_task(function* () {
     ]};
     SpecialPowers.pushPrefEnv(options, done);
   });
 
   let { tab, document } = yield openAboutDebugging("workers");
 
   let swTab = yield addTab(TAB_URL);
 
-  let serviceWorkersElement = document.getElementById("service-workers");
+  let serviceWorkersElement = getServiceWorkerList(document);
 
   yield waitForMutation(serviceWorkersElement, { childList: true });
 
   // Check that the service worker appears in the UI
   let names = [...document.querySelectorAll("#service-workers .target-name")];
   names = names.map(element => element.textContent);
   ok(names.includes(SERVICE_WORKER),
     "The service worker url appears in the list: " + names);
--- a/devtools/client/aboutdebugging/test/browser_service_workers_push.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_push.js
@@ -25,17 +25,17 @@ add_task(function* () {
       ["dom.serviceWorkers.testing.enabled", true],
     ]};
     SpecialPowers.pushPrefEnv(options, done);
   });
 
   let { tab, document } = yield openAboutDebugging("workers");
 
   // Listen for mutations in the service-workers list.
-  let serviceWorkersElement = document.getElementById("service-workers");
+  let serviceWorkersElement = getServiceWorkerList(document);
   let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
 
   // Open a tab that registers a push service worker.
   let swTab = yield addTab(TAB_URL);
 
   info("Make the test page notify us when the service worker sends a message.");
   let frameScript = function() {
     let win = content.wrappedJSObject;
--- a/devtools/client/aboutdebugging/test/browser_service_workers_start.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_start.js
@@ -27,17 +27,17 @@ add_task(function* () {
       ["dom.serviceWorkers.idle_extended_timeout", SW_TIMEOUT],
     ]};
     SpecialPowers.pushPrefEnv(options, done);
   });
 
   let { tab, document } = yield openAboutDebugging("workers");
 
   // Listen for mutations in the service-workers list.
-  let serviceWorkersElement = document.getElementById("service-workers");
+  let serviceWorkersElement = getServiceWorkerList(document);
   let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
 
   // Open a tab that registers an empty service worker.
   let swTab = yield addTab(TAB_URL);
 
   // Wait for the service-workers list to update.
   yield onMutation;
 
--- a/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_timeout.js
@@ -24,17 +24,17 @@ add_task(function* () {
     ]};
     SpecialPowers.pushPrefEnv(options, done);
   });
 
   let { tab, document } = yield openAboutDebugging("workers");
 
   let swTab = yield addTab(TAB_URL);
 
-  let serviceWorkersElement = document.getElementById("service-workers");
+  let serviceWorkersElement = getServiceWorkerList(document);
   yield waitForMutation(serviceWorkersElement, { childList: true });
 
   assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
 
   // Ensure that the registration resolved before trying to connect to the sw
   yield waitForServiceWorkerRegistered(swTab);
   ok(true, "Service worker registration resolved");
 
--- a/devtools/client/aboutdebugging/test/browser_service_workers_unregister.js
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_unregister.js
@@ -27,17 +27,17 @@ add_task(function* () {
       ["dom.serviceWorkers.testing.enabled", true],
     ]};
     SpecialPowers.pushPrefEnv(options, done);
   });
 
   let { tab, document } = yield openAboutDebugging("workers");
 
   // Listen for mutations in the service-workers list.
-  let serviceWorkersElement = document.getElementById("service-workers");
+  let serviceWorkersElement = getServiceWorkerList(document);
   let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
 
   // Open a tab that registers an empty service worker.
   let swTab = yield addTab(TAB_URL);
 
   // Wait for the service workers-list to update.
   yield onMutation;
 
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* eslint-env browser */
 /* eslint-disable mozilla/no-cpows-in-tests */
 /* exported openAboutDebugging, closeAboutDebugging, installAddon,
-   uninstallAddon, waitForMutation, assertHasTarget,
+   uninstallAddon, waitForMutation, assertHasTarget, getServiceWorkerList,
    waitForInitialAddonList, waitForServiceWorkerRegistered,
    unregisterServiceWorker */
 /* global sendAsyncMessage */
 
 "use strict";
 
 var { utils: Cu, classes: Cc, interfaces: Ci } = Components;
 
@@ -88,24 +88,46 @@ function removeTab(tab, win) {
 function getSupportsFile(path) {
   let cr = Cc["@mozilla.org/chrome/chrome-registry;1"]
     .getService(Ci.nsIChromeRegistry);
   let uri = Services.io.newURI(CHROME_ROOT + path, null, null);
   let fileurl = cr.convertChromeURL(uri);
   return fileurl.QueryInterface(Ci.nsIFileURL);
 }
 
+/**
+ * Depending on whether there are addons installed, return either a target list
+ * element or its container.
+ * @param  {DOMDocument}  document   #addons section container document
+ * @return {DOMNode}                 target list or container element
+ */
+function getAddonList(document) {
+  return document.querySelector("#addons .target-list") ||
+    document.querySelector("#addons .targets");
+}
+
+/**
+ * Depending on whether there are service workers installed, return either a
+ * target list element or its container.
+ * @param  {DOMDocument}  document   #service-workers section container document
+ * @return {DOMNode}                 target list or container element
+ */
+function getServiceWorkerList(document) {
+  return document.querySelector("#service-workers .target-list") ||
+    document.querySelector("#service-workers.targets");
+}
+
 function* installAddon(document, path, name, evt) {
   // Mock the file picker to select a test addon
   let MockFilePicker = SpecialPowers.MockFilePicker;
   MockFilePicker.init(null);
   let file = getSupportsFile(path);
   MockFilePicker.returnFiles = [file.file];
 
-  let addonList = document.querySelector("#addons .targets");
+  let addonList = getAddonList(document);
   let addonListMutation = waitForMutation(addonList, { childList: true });
 
   // Wait for a message sent by the addon's bootstrap.js file
   let onAddonInstalled = new Promise(done => {
     Services.obs.addObserver(function listener() {
       Services.obs.removeObserver(listener, evt);
 
       done();
@@ -121,17 +143,17 @@ function* installAddon(document, path, n
   yield addonListMutation;
   let names = [...addonList.querySelectorAll(".target-name")];
   names = names.map(element => element.textContent);
   ok(names.includes(name),
     "The addon name appears in the list of addons: " + names);
 }
 
 function* uninstallAddon(document, addonId, addonName) {
-  let addonList = document.querySelector("#addons .targets");
+  let addonList = getAddonList(document);
   let addonListMutation = waitForMutation(addonList, { childList: true });
 
   // Now uninstall this addon
   yield new Promise(done => {
     AddonManager.getAddonByID(addonId, addon => {
       let listener = {
         onUninstalled: function(uninstalledAddon) {
           if (uninstalledAddon != addon) {
@@ -158,17 +180,17 @@ function* uninstallAddon(document, addon
 
 /**
  * Returns a promise that will resolve when the add-on list has been updated.
  *
  * @param {Node} document
  * @return {Promise}
  */
 function waitForInitialAddonList(document) {
-  const addonListContainer = document.querySelector("#addons .targets");
+  const addonListContainer = getAddonList(document);
   let addonCount = addonListContainer.querySelectorAll(".target");
   addonCount = addonCount ? [...addonCount].length : -1;
   info("Waiting for add-ons to load. Current add-on count: " + addonCount);
 
   // This relies on the network speed of the actor responding to the
   // listAddons() request and also the speed of openAboutDebugging().
   let result;
   if (addonCount > 0) {