Bug 1453093 - move accessible actor instantiation and panel tab highlighting into tool startup component. r=ochameau
authorYura Zenevich <yura.zenevich@gmail.com>
Mon, 04 Jun 2018 09:30:44 -0400
changeset 422026 621b7afe0d4a78a7ba897dd4d7734eb25a137cf9
parent 422025 ee8699581c1666e449ee5a2fc15ea6ecf664e97a
child 422027 0130f657be99498548ee315464186a39f4aa54ec
push id34114
push userbtara@mozilla.com
push dateSat, 09 Jun 2018 15:31:58 +0000
treeherdermozilla-central@e02a5155d815 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1453093
milestone62.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 1453093 - move accessible actor instantiation and panel tab highlighting into tool startup component. r=ochameau MozReview-Commit-ID: F02RgSyupUQ
devtools/client/accessibility/accessibility-panel.js
devtools/client/accessibility/accessibility-startup.js
devtools/client/accessibility/accessibility-view.js
devtools/client/accessibility/moz.build
devtools/client/accessibility/picker.js
devtools/client/accessibility/reducers/ui.js
devtools/client/accessibility/test/browser/browser.ini
devtools/client/accessibility/test/browser/browser_accessibility_panel_highlighter.js
devtools/client/accessibility/test/browser/browser_accessibility_panel_highlighter_multi_tab.js
devtools/client/accessibility/test/browser/head.js
devtools/client/definitions.js
devtools/client/framework/components/ToolboxController.js
devtools/client/framework/toolbox.js
devtools/client/inspector/test/head.js
devtools/client/inspector/test/shared-head.js
--- a/devtools/client/accessibility/accessibility-panel.js
+++ b/devtools/client/accessibility/accessibility-panel.js
@@ -1,14 +1,13 @@
 /* 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 { AccessibilityFront } = require("devtools/shared/fronts/accessibility");
 const EventEmitter = require("devtools/shared/event-emitter");
 
 const Telemetry = require("devtools/client/shared/telemetry");
 
 const { Picker } = require("./picker");
 const { A11Y_SERVICE_DURATION } = require("./constants");
 
 // The panel's window global is an EventEmitter firing the following events:
@@ -23,29 +22,29 @@ const EVENTS = {
   ACCESSIBILITY_INSPECTOR_UPDATED: "Accessibility:AccessibilityInspectorUpdated"
 };
 
 /**
  * This object represents Accessibility panel. It's responsibility is to
  * render Accessibility Tree of the current debugger target and the sidebar that
  * displays current relevant accessible details.
  */
-function AccessibilityPanel(iframeWindow, toolbox) {
+function AccessibilityPanel(iframeWindow, toolbox, startup) {
   this.panelWin = iframeWindow;
   this._toolbox = toolbox;
+  this.startup = startup;
 
   this.onTabNavigated = this.onTabNavigated.bind(this);
   this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
   this.onNewAccessibleFrontSelected =
     this.onNewAccessibleFrontSelected.bind(this);
   this.onAccessibilityInspectorUpdated =
     this.onAccessibilityInspectorUpdated.bind(this);
   this.updateA11YServiceDurationTimer = this.updateA11YServiceDurationTimer.bind(this);
   this.updatePickerButton = this.updatePickerButton.bind(this);
-  this.updateToolboxButtons = this.updateToolboxButtons.bind(this);
 
   EventEmitter.decorate(this);
 }
 
 AccessibilityPanel.prototype = {
   /**
    * Open is effectively an asynchronous constructor.
    */
@@ -77,29 +76,24 @@ AccessibilityPanel.prototype = {
       this.onNewAccessibleFrontSelected);
     this.panelWin.on(EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED,
       this.onAccessibilityInspectorUpdated);
 
     this.shouldRefresh = true;
     this.panelWin.gToolbox = this._toolbox;
 
     await this._toolbox.initInspector();
-    this._front = new AccessibilityFront(this.target.client,
-                                         this.target.form);
-    this._walker = await this._front.getWalker();
-
-    this._isOldVersion = !(await this.target.actorHasMethod("accessibility", "enable"));
-    if (!this._isOldVersion) {
-      await this._front.bootstrap();
+    await this.startup.initAccessibility();
+    if (this.supportsLatestAccessibility) {
       this.picker = new Picker(this);
     }
 
     this.updateA11YServiceDurationTimer();
-    this._front.on("init", this.updateA11YServiceDurationTimer);
-    this._front.on("shutdown", this.updateA11YServiceDurationTimer);
+    this.front.on("init", this.updateA11YServiceDurationTimer);
+    this.front.on("shutdown", this.updateA11YServiceDurationTimer);
 
     this.isReady = true;
     this.emit("ready");
     resolver(this);
     return this._opening;
   },
 
   onNewAccessibleFrontSelected(selected) {
@@ -125,92 +119,93 @@ AccessibilityPanel.prototype = {
    */
   onPanelVisibilityChange() {
     this._opening.then(() => this.refresh());
   },
 
   refresh() {
     this.cancelPicker();
 
-    if (this.isVisible) {
-      this._front.on("init", this.updateToolboxButtons);
-      this._front.on("shutdown", this.updateToolboxButtons);
-    } else {
-      this._front.off("init", this.updateToolboxButtons);
-      this._front.off("shutdown", this.updateToolboxButtons);
+    if (!this.isVisible) {
       // Do not refresh if the panel isn't visible.
       return;
     }
 
     // Do not refresh if it isn't necessary.
     if (!this.shouldRefresh) {
       return;
     }
     // Alright reset the flag we are about to refresh the panel.
     this.shouldRefresh = false;
-    this.postContentMessage("initialize", this._front, this._walker, this._isOldVersion);
+    this.postContentMessage("initialize", this.front,
+                                          this.walker,
+                                          this.supportsLatestAccessibility);
   },
 
   updateA11YServiceDurationTimer() {
-    if (this._front.enabled) {
+    if (this.front.enabled) {
       this._telemetry.start(A11Y_SERVICE_DURATION, this, true);
     } else {
       this._telemetry.finish(A11Y_SERVICE_DURATION, this, true);
     }
   },
 
   selectAccessible(accessibleFront) {
-    this.postContentMessage("selectAccessible", this._walker, accessibleFront);
+    this.postContentMessage("selectAccessible", this.walker, accessibleFront);
   },
 
   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);
   },
 
   highlightAccessible(accessibleFront) {
-    this.postContentMessage("highlightAccessible", this._walker, accessibleFront);
+    this.postContentMessage("highlightAccessible", this.walker, accessibleFront);
   },
 
   postContentMessage(type, ...args) {
     const event = new this.panelWin.MessageEvent("devtools/chrome/message", {
       bubbles: true,
       cancelable: true,
       data: { type, args }
     });
 
     this.panelWin.dispatchEvent(event);
   },
 
-  updateToolboxButtons() {
-    this._toolbox.updatePickerButton();
-  },
-
   updatePickerButton() {
     this.picker && this.picker.updateButton();
   },
 
   togglePicker(focus) {
     this.picker && this.picker.toggle();
   },
 
   cancelPicker() {
     this.picker && this.picker.cancel();
   },
 
   stopPicker() {
     this.picker && this.picker.stop();
   },
 
+  get front() {
+    return this.startup.accessibility;
+  },
+
   get walker() {
-    return this._walker;
+    return this.startup.walker;
+  },
+
+  get supportsLatestAccessibility() {
+    return this.startup._supportsLatestAccessibility;
   },
 
   /**
    * Return true if the Accessibility panel is currently selected.
    */
   get isVisible() {
     return this._toolbox.currentToolId === "accessibility";
   },
@@ -236,23 +231,21 @@ AccessibilityPanel.prototype = {
     this.panelWin.off(EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED,
       this.onNewAccessibleFrontSelected);
     this.panelWin.off(EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED,
       this.onAccessibilityInspectorUpdated);
 
     this.picker.release();
     this.picker = null;
 
-    if (this._front) {
-      this._front.off("init", this.updateA11YServiceDurationTimer);
-      this._front.off("shutdown", this.updateA11YServiceDurationTimer);
-      await this._front.destroy();
+    if (this.front) {
+      this.front.off("init", this.updateA11YServiceDurationTimer);
+      this.front.off("shutdown", this.updateA11YServiceDurationTimer);
     }
 
-    this._front = null;
     this._telemetry = null;
     this.panelWin.gToolbox = null;
     this.panelWin.gTelemetry = null;
 
     this.emit("destroyed");
 
     resolver();
   }
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/accessibility-startup.js
@@ -0,0 +1,138 @@
+/* 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 { AccessibilityFront } = require("devtools/shared/fronts/accessibility");
+
+/**
+ * Component responsible for all accessibility panel startup steps before the panel is
+ * actually opened.
+ */
+class AccessibilityStartup {
+  constructor(toolbox) {
+    this.toolbox = toolbox;
+
+    this._updateAccessibilityState = this._updateAccessibilityState.bind(this);
+
+    // Creates accessibility front.
+    this.initAccessibility();
+  }
+
+  get target() {
+    return this.toolbox.target;
+  }
+
+  /**
+   * Get the accessibility front for the toolbox.
+   */
+  get accessibility() {
+    return this._accessibility;
+  }
+
+  get walker() {
+    return this._walker;
+  }
+
+  /**
+   * Fully initialize accessibility front. Also add listeners for accessibility
+   * service lifecycle events that affect picker state and the state of the tool tab
+   * highlight.
+   * @return {Promise}
+   *         A promise for when accessibility front is fully initialized.
+   */
+  initAccessibility() {
+    if (!this._initAccessibility) {
+      this._initAccessibility = (async function() {
+        this._accessibility = new AccessibilityFront(this.target.client,
+                                                     this.target.form);
+        // We must call a method on an accessibility front here (such as getWalker), in
+        // oreder to be able to check actor's backward compatibility via actorHasMethod.
+        // See targe.js@getActorDescription for more information.
+        this._walker = await this._accessibility.getWalker();
+        // Only works with FF61+ targets
+        this._supportsLatestAccessibility =
+          await this.target.actorHasMethod("accessibility", "enable");
+
+        if (this._supportsLatestAccessibility) {
+          await this._accessibility.bootstrap();
+        }
+
+        this._updateAccessibilityState();
+        this._accessibility.on("init", this._updateAccessibilityState);
+        this._accessibility.on("shutdown", this._updateAccessibilityState);
+      }.bind(this))();
+    }
+
+    return this._initAccessibility;
+  }
+
+  /**
+   * Destroy accessibility front. Also remove listeners for accessibility service
+   * lifecycle events.
+   * @return {Promise}
+   *         A promise for when accessibility front is fully destroyed.
+   */
+  destroyAccessibility() {
+    if (this._destroyingAccessibility) {
+      return this._destroyingAccessibility;
+    }
+
+    this._destroyingAccessibility = (async function() {
+      if (!this._accessibility) {
+        return;
+      }
+
+      // Ensure that the accessibility isn't still being initiated, otherwise race
+      // conditions in the initialization process can throw errors.
+      await this._initAccessibility;
+
+      this._accessibility.off("init", this._updateAccessibilityState);
+      this._accessibility.off("shutdown", this._updateAccessibilityState);
+
+      await this._walker.destroy();
+      await this._accessibility.destroy();
+      this._accessibility = null;
+      this._walker = null;
+    }.bind(this))();
+    return this._destroyingAccessibility;
+  }
+
+  /**
+   * Update states of the accessibility picker and accessibility tab highlight.
+   * @return {[type]} [description]
+   */
+  _updateAccessibilityState() {
+    this._updateAccessibilityToolHighlight();
+    this._updatePickerButton();
+  }
+
+  /**
+   * Update picker button state and ensure toolbar is re-rendered correctly.
+   */
+  _updatePickerButton() {
+    this.toolbox.updatePickerButton();
+    // Calling setToolboxButtons to make sure toolbar is re-rendered correctly.
+    this.toolbox.component.setToolboxButtons(this.toolbox.toolbarButtons);
+  }
+
+  /**
+   * Set the state of the accessibility tab highlight depending on whether the
+   * accessibility service is initialized or shutdown.
+   */
+  _updateAccessibilityToolHighlight() {
+    if (this._accessibility.enabled) {
+      this.toolbox.highlightTool("accessibility");
+    } else {
+      this.toolbox.unhighlightTool("accessibility");
+    }
+  }
+
+  async destroy() {
+    await this.destroyAccessibility();
+    this.toolbox = null;
+  }
+}
+
+exports.AccessibilityStartup = AccessibilityStartup;
--- a/devtools/client/accessibility/accessibility-view.js
+++ b/devtools/client/accessibility/accessibility-view.js
@@ -43,22 +43,22 @@ AccessibilityView.prototype = {
   /**
    * Initialize accessibility view, create its top level component and set the
    * data store.
    *
    * @param {Object} accessibility  front that can initialize accessibility
    *                                walker and enable/disable accessibility
    *                                services.
    */
-  async initialize(accessibility, walker, isOldVersion) {
+  async initialize(accessibility, walker, supportsLatestAccessibility) {
     // Make sure state is reset every time accessibility panel is initialized.
     await this.store.dispatch(reset(accessibility));
     const container = document.getElementById("content");
 
-    if (isOldVersion) {
+    if (!supportsLatestAccessibility) {
       ReactDOM.render(OldVersionDescription(), container);
       return;
     }
 
     const mainFrame = MainFrame({ accessibility, walker });
     // Render top level component
     const provider = createElement(Provider, { store: this.store }, mainFrame);
     this.mainFrame = ReactDOM.render(provider, container);
--- a/devtools/client/accessibility/moz.build
+++ b/devtools/client/accessibility/moz.build
@@ -9,16 +9,17 @@ DIRS += [
     'actions',
     'components',
     'reducers',
     'utils'
 ]
 
 DevToolsModules(
     'accessibility-panel.js',
+    'accessibility-startup.js',
     'accessibility-view.js',
     'accessibility.css',
     'constants.js',
     'picker.js',
     'provider.js',
 )
 
 with Files('**'):
--- a/devtools/client/accessibility/picker.js
+++ b/devtools/client/accessibility/picker.js
@@ -18,17 +18,17 @@ class Picker {
     this.onPickerAccessibleCanceled = this.onPickerAccessibleCanceled.bind(this);
   }
 
   get toolbox() {
     return this._panel._toolbox;
   }
 
   get walker() {
-    return this._panel._walker;
+    return this._panel.walker;
   }
 
   get pickerButton() {
     return this.toolbox.pickerButton;
   }
 
   get _telemetry() {
     return this._panel._telemetry;
@@ -66,18 +66,18 @@ class Picker {
 
   /**
    * Override the default presentation of the picker button in toolbox's top
    * level toolbar.
    */
   updateButton() {
     this.pickerButton.description = this.getStr("accessibility.pick");
     this.pickerButton.className = "accessibility";
-    this.pickerButton.disabled = !this._panel._front.enabled;
-    if (!this._panel._front.enabled && this.isPicking) {
+    this.pickerButton.disabled = !this._panel.front.enabled;
+    if (!this._panel.front.enabled && this.isPicking) {
       this.cancel();
     }
   }
 
   /**
    * Handle an event when a new accessible object is hovered over.
    * @param  {Object} accessible
    *         newly hovered accessible object
--- a/devtools/client/accessibility/reducers/ui.js
+++ b/devtools/client/accessibility/reducers/ui.js
@@ -1,15 +1,13 @@
 /* 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";
 
-/* global gToolbox */
-
 const {
   ENABLE,
   DISABLE,
   RESET,
   SELECT,
   HIGHLIGHT,
   UNHIGHLIGHT,
   UPDATE_CAN_BE_DISABLED,
@@ -131,38 +129,28 @@ function onCanBeEnabledChange(state, { c
 /**
  * Handle reset action for the accessibility panel UI.
  * @param  {Object}  state   Current ui state.
  * @param  {Object}  action  Redux action object
  * @return {Object}  updated state
  */
 function onReset(state, { accessibility }) {
   const { enabled, canBeDisabled, canBeEnabled } = accessibility;
-  toggleHighlightTool(enabled);
   return Object.assign({}, state, { enabled, canBeDisabled, canBeEnabled });
 }
 
 /**
  * Handle accessibilty service enabling/disabling.
  * @param {Object}  state   Current accessibility services enabled state.
  * @param {Object}  action  Redux action object
  * @param {Boolean} enabled New enabled state.
  * @return {Object}  updated state
  */
 function onToggle(state, { error }, enabled) {
   if (error) {
     console.warn("Error enabling accessibility service: ", error);
     return state;
   }
 
-  toggleHighlightTool(enabled);
   return Object.assign({}, state, { enabled });
 }
 
-function toggleHighlightTool(enabled) {
-  if (enabled) {
-    gToolbox.highlightTool("accessibility");
-  } else {
-    gToolbox.unhighlightTool("accessibility");
-  }
-}
-
 exports.ui = ui;
--- a/devtools/client/accessibility/test/browser/browser.ini
+++ b/devtools/client/accessibility/test/browser/browser.ini
@@ -6,12 +6,14 @@ support-files =
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/shared-redux-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
 
 [browser_accessibility_context_menu_browser.js]
 [browser_accessibility_context_menu_inspector.js]
 [browser_accessibility_mutations.js]
+[browser_accessibility_panel_highlighter.js]
+[browser_accessibility_panel_highlighter_multi_tab.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/browser_accessibility_panel_highlighter.js
@@ -0,0 +1,31 @@
+/* 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>";
+
+add_task(async function tabNotHighlighted() {
+  await addTab(buildURL(TEST_URI));
+  const { toolbox } = await openInspector();
+  const isHighlighted = await toolbox.isToolHighlighted("accessibility");
+
+  ok(!isHighlighted, "When accessibility service is not running, accessibility panel " +
+                     "should not be highlighted when toolbox opens");
+
+  gBrowser.removeCurrentTab();
+});
+
+add_task(async function tabHighlighted() {
+  let a11yService = await initA11y();
+  ok(a11yService, "Accessibility service was started");
+  await addTab(buildURL(TEST_URI));
+  const { toolbox } = await openInspector();
+  const isHighlighted = await toolbox.isToolHighlighted("accessibility");
+
+  ok(isHighlighted, "When accessibility service is running, accessibility panel should" +
+                    "be highlighted when toolbox opens");
+
+  a11yService = null;
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/test/browser/browser_accessibility_panel_highlighter_multi_tab.js
@@ -0,0 +1,62 @@
+/* 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>";
+
+add_task(async function() {
+  const { toolbox: toolbox1 } = await addTestTab(buildURL(TEST_URI));
+  const { toolbox: toolbox2 } = await addTestTab(buildURL(TEST_URI));
+  const options = await openOptions(toolbox2);
+
+  info("Check that initially both accessibility panels are highlighted.");
+  await checkHighlighted(toolbox1, true);
+  await checkHighlighted(toolbox2, true);
+
+  info("Toggle accessibility panel off an on.");
+  await toggleAccessibility(options);
+  await toggleAccessibility(options);
+
+  await checkHighlighted(toolbox1, true);
+  await checkHighlighted(toolbox2, true);
+
+  info("Toggle accessibility panel off an on again.");
+  await toggleAccessibility(options);
+  await toggleAccessibility(options);
+
+  const panel = await toolbox2.selectTool("accessibility");
+  await disableAccessibilityInspector(
+    { panel, win: panel.panelWin, doc: panel.panelWin.document });
+
+  await checkHighlighted(toolbox1, false);
+  await checkHighlighted(toolbox2, false);
+});
+
+async function checkHighlighted(toolbox, expected) {
+  await BrowserTestUtils.waitForCondition(async function() {
+    const isHighlighted = await toolbox.isToolHighlighted("accessibility");
+    return isHighlighted === expected;
+  });
+}
+
+async function openOptions(toolbox) {
+  const panel = await toolbox.selectTool("options");
+  return {
+    panelWin: panel.panelWin,
+    // This is a getter becuse toolbox tools list gets re-setup every time there
+    // is a tool-registered or tool-undregistered event.
+    get checkbox() {
+      return panel.panelDoc.getElementById("accessibility");
+    }
+  };
+}
+
+async function toggleAccessibility({ panelWin, checkbox }) {
+  const prevChecked = checkbox.checked;
+  const onToggleTool = gDevTools.once(
+    `tool-${prevChecked ? "unregistered" : "registered"}`);
+  EventUtils.sendMouseEvent({ type: "click" }, checkbox, panelWin);
+  const id = await onToggleTool;
+  is(id, "accessibility", "Correct event was fired");
+}
--- a/devtools/client/accessibility/test/browser/head.js
+++ b/devtools/client/accessibility/test/browser/head.js
@@ -26,16 +26,40 @@ Services.scriptloader.loadSubScript(
   this);
 
 const { ORDERED_PROPS } = require("devtools/client/accessibility/constants");
 
 // Enable the Accessibility panel
 Services.prefs.setBoolPref("devtools.accessibility.enabled", true);
 
 /**
+ * Enable accessibility service and wait for a11y init event.
+ * @return {Object}  instance of accessibility service.
+ */
+async function initA11y() {
+  if (Services.appinfo.accessibilityEnabled) {
+    return Cc["@mozilla.org/accessibilityService;1"].getService(
+      Ci.nsIAccessibilityService);
+  }
+
+  const initPromise = new Promise(resolve => {
+    const observe = () => {
+      Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+      resolve();
+    };
+    Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+  });
+
+  const a11yService = Cc["@mozilla.org/accessibilityService;1"].getService(
+    Ci.nsIAccessibilityService);
+  await initPromise;
+  return a11yService;
+}
+
+/**
  * 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".
  */
 function shutdownA11y() {
   if (!Services.appinfo.accessibilityEnabled) {
     return Promise.resolve();
   }
 
@@ -78,18 +102,21 @@ async function addTestTab(url) {
   info("Adding a new test tab with URL: '" + url + "'");
 
   const tab = await addTab(url);
   const panel = await initAccessibilityPanel(tab);
   const win = panel.panelWin;
   const doc = win.document;
   const store = win.view.store;
 
-  EventUtils.sendMouseEvent({ type: "click" },
-    doc.getElementById("accessibility-enable-button"), win);
+  const enableButton = doc.getElementById("accessibility-enable-button");
+  // If enable button is not found, asume the tool is already enabled.
+  if (enableButton) {
+    EventUtils.sendMouseEvent({ type: "click" }, enableButton, win);
+  }
 
   await waitUntilState(store, state =>
     state.accessibles.size === 1 && state.details.accessible &&
     state.details.accessible.role === "document");
 
   // Wait for inspector load here to avoid protocol errors on shutdown, since
   // accessibility panel test can be too fast.
   await win.gToolbox.loadTool("inspector");
@@ -108,19 +135,20 @@ async function addTestTab(url) {
 /**
  * Turn off accessibility features from within the panel. We call it before the
  * cleanup function to make sure that the panel is still present.
  */
 async function disableAccessibilityInspector(env) {
   const { doc, win, panel } = env;
   // Disable accessibility service through the panel and wait for the shutdown
   // event.
-  const shutdown = panel._front.once("shutdown");
-  EventUtils.sendMouseEvent({ type: "click" },
-    doc.getElementById("accessibility-disable-button"), win);
+  const shutdown = panel.front.once("shutdown");
+  const disableButton = await BrowserTestUtils.waitForCondition(() =>
+    doc.getElementById("accessibility-disable-button"), "Wait for the disable button.");
+  EventUtils.sendMouseEvent({ type: "click" }, disableButton, win);
   await shutdown;
 }
 
 /**
  * Open the Accessibility panel for the given tab.
  *
  * @param {Element} tab
  *        Optional tab element for which you want open the Accessibility panel.
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -23,16 +23,17 @@ loader.lazyGetter(this, "NewPerformanceP
 loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/client/netmonitor/panel").NetMonitorPanel);
 loader.lazyGetter(this, "StoragePanel", () => require("devtools/client/storage/panel").StoragePanel);
 loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/client/scratchpad/scratchpad-panel").ScratchpadPanel);
 loader.lazyGetter(this, "DomPanel", () => require("devtools/client/dom/dom-panel").DomPanel);
 loader.lazyGetter(this, "AccessibilityPanel", () => require("devtools/client/accessibility/accessibility-panel").AccessibilityPanel);
 loader.lazyGetter(this, "ApplicationPanel", () => require("devtools/client/application/panel").ApplicationPanel);
 
 // Other dependencies
+loader.lazyRequireGetter(this, "AccessibilityStartup", "devtools/client/accessibility/accessibility-startup", true);
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
 loader.lazyRequireGetter(this, "CommandState", "devtools/shared/gcli/command-state", true);
 loader.lazyRequireGetter(this, "ResponsiveUIManager", "devtools/client/responsive.html/manager", true);
 loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 
 const {MultiLocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new MultiLocalizationHelper(
   "devtools/client/locales/startup.properties",
@@ -447,17 +448,22 @@ Tools.accessibility = {
   },
   inMenu: true,
 
   isTargetSupported(target) {
     return target.hasActor("accessibility");
   },
 
   build(iframeWindow, toolbox) {
-    return new AccessibilityPanel(iframeWindow, toolbox);
+    const startup = toolbox.getToolStartup("accessibility");
+    return new AccessibilityPanel(iframeWindow, toolbox, startup);
+  },
+
+  buildToolStartup(toolbox) {
+    return new AccessibilityStartup(toolbox);
   }
 };
 
 Tools.application = {
   id: "application",
   ordinal: 15,
   visibilityswitch: "devtools.application.enabled",
   icon: "chrome://devtools/skin/images/tool-application.svg",
--- a/devtools/client/framework/components/ToolboxController.js
+++ b/devtools/client/framework/components/ToolboxController.js
@@ -113,16 +113,20 @@ class ToolboxController extends Componen
       this.setFocusedButton(currentToolId);
     });
   }
 
   setCanRender() {
     this.setState({ canRender: true }, this.updateButtonIds);
   }
 
+  isToolHighlighted(toolID) {
+    return this.state.highlightedTools.has(toolID);
+  }
+
   highlightTool(highlightedTool) {
     const { highlightedTools } = this.state;
     highlightedTools.add(highlightedTool);
     this.setState({ highlightedTools });
   }
 
   unhighlightTool(id) {
     const { highlightedTools } = this.state;
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -107,16 +107,18 @@ function Toolbox(target, selectedTool, h
   this.frameId = frameId;
   this.telemetry = new Telemetry();
 
   // Map of the available DevTools WebExtensions:
   //   Map<extensionUUID, extensionName>
   this._webExtensions = new Map();
 
   this._toolPanels = new Map();
+  // Map of tool startup components for given tool id.
+  this._toolStartups = new Map();
   this._inspectorExtensionSidebars = new Map();
 
   this._initInspector = null;
   this._inspector = null;
   this._styleSheets = null;
   this._netMonitorAPI = null;
 
   // Map of frames (id => frame-info) and currently selected frame id.
@@ -1433,16 +1435,20 @@ Toolbox.prototype = {
 
     // There is already a container for the webconsole frame.
     if (!this.doc.getElementById("toolbox-panel-" + id)) {
       panel.id = "toolbox-panel-" + id;
     }
 
     deck.appendChild(panel);
 
+    if (toolDefinition.buildToolStartup && !this._toolStartups.has(id)) {
+      this._toolStartups.set(id, toolDefinition.buildToolStartup(this));
+    }
+
     this._addKeysToWindow();
   },
 
   /**
    * Lazily created map of the additional tools registered to this toolbox.
    *
    * @returns {Map<string, object>}
    *          a map of the tools definitions registered to this
@@ -2076,16 +2082,29 @@ Toolbox.prototype = {
     const index = definitions.findIndex(({id}) => id === this.currentToolId);
     const definition = index === -1 || index < 1
                      ? definitions[definitions.length - 1]
                      : definitions[index - 1];
     return this.selectTool(definition.id, "select_prev_key");
   },
 
   /**
+   * Check if the tool's tab is highlighted.
+   *
+   * @param {string} id
+   *        The id of the tool to be checked
+   */
+  async isToolHighlighted(id) {
+    if (!this.component) {
+      await this.isOpen;
+    }
+    return this.component.isToolHighlighted(id);
+  },
+
+  /**
    * Highlights the tool's tab if it is not the currently selected tool.
    *
    * @param {string} id
    *        The id of the tool to highlight
    */
   async highlightTool(id) {
     if (!this.component) {
       await this.isOpen;
@@ -2585,16 +2604,35 @@ Toolbox.prototype = {
       const key = doc.getElementById("key_" + toolId);
       if (key) {
         key.remove();
       }
     }
   },
 
   /**
+   * Get a startup component for a given tool.
+  * @param  {string} toolId
+   *         Id of the tool to get the startup component for.
+   */
+  getToolStartup: function(toolId) {
+    return this._toolStartups.get(toolId);
+  },
+
+  _unloadToolStartup: async function(toolId) {
+    const startup = this.getToolStartup(toolId);
+    if (!startup) {
+      return;
+    }
+
+    this._toolStartups.delete(toolId);
+    await startup.destroy();
+  },
+
+  /**
    * Handler for the tool-registered event.
    * @param  {string} toolId
    *         Id of the tool that was registered
    */
   _toolRegistered: function(toolId) {
     // Tools can either be in the global devtools, or added to this specific toolbox
     // as an additional tool.
     let definition = gDevTools.getToolDefinition(toolId);
@@ -2621,16 +2659,18 @@ Toolbox.prototype = {
 
   /**
    * Handler for the tool-unregistered event.
    * @param  {string} toolId
    *         id of the tool that was unregistered
    */
   _toolUnregistered: function(toolId) {
     this.unloadTool(toolId);
+    this._unloadToolStartup(toolId);
+
     // Emit the event so tools can listen to it from the toolbox level
     // instead of gDevTools
     this.emit("tool-unregistered", toolId);
   },
 
   /**
    * Initialize the inspector/walker/selection/highlighter fronts.
    * Returns a promise that resolves when the fronts are initialized
@@ -2833,16 +2873,20 @@ Toolbox.prototype = {
 
         outstanding.push(panel.destroy());
       } catch (e) {
         // We don't want to stop here if any panel fail to close.
         console.error("Panel " + id + ":", e);
       }
     }
 
+    for (const id of this._toolStartups.keys()) {
+      outstanding.push(this._unloadToolStartup(id));
+    }
+
     this.browserRequire = null;
 
     // Now that we are closing the toolbox we can re-enable the cache settings
     // and disable the service workers testing settings for the current tab.
     // FF41+ automatically cleans up state in actor on disconnect.
     if (this.target.activeTab && !this.target.activeTab.traits.noTabReconfigureOnClose) {
       this.target.activeTab.reconfigure({
         "cacheDisabled": false,
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -1,33 +1,27 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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/. */
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
 /* import-globals-from ../../shared/test/shared-head.js */
-/* import-globals-from ../../shared/test/test-actor-registry.js */
 /* import-globals-from ../../inspector/test/shared-head.js */
 "use strict";
 
 // Load the shared-head file first.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
   this);
 
 // Services.prefs.setBoolPref("devtools.debugger.log", true);
 // SimpleTest.registerCleanupFunction(() => {
 //   Services.prefs.clearUserPref("devtools.debugger.log");
 // });
 
-// Import helpers registering the test-actor in remote targets
-Services.scriptloader.loadSubScript(
-  "chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
-  this);
-
 // Import helpers for the inspector that are also shared with others
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
   this);
 
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
       new LocalizationHelper("devtools/client/locales/inspector.properties");
--- a/devtools/client/inspector/test/shared-head.js
+++ b/devtools/client/inspector/test/shared-head.js
@@ -2,16 +2,22 @@
  * 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";
 
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
 /* globals registerTestActor, getTestActor, openToolboxForTab, gBrowser */
 /* import-globals-from ../../shared/test/shared-head.js */
+/* import-globals-from ../../shared/test/test-actor-registry.js */
+
+// Import helpers registering the test-actor in remote targets
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
+  this);
 
 var {getInplaceEditorForSpan: inplaceEditor} = require("devtools/client/shared/inplace-editor");
 
 // This file contains functions related to the inspector that are also of interest to
 // other test directores as well.
 
 /**
  * Open the toolbox, with the inspector tool visible.