Bug 1453093 - Move accessible actor instantiation and panel tab highlighting into tool startup component. r=ochameau, a=RyanVM DEVEDITION_61_0b13_BUILD1 DEVEDITION_61_0b13_RELEASE FENNEC_61_0b13_BUILD1 FENNEC_61_0b13_RELEASE FIREFOX_61_0b13_BUILD1 FIREFOX_61_0b13_RELEASE
authorYura Zenevich <yura.zenevich@gmail.com>
Mon, 11 Jun 2018 09:44:18 -0400
changeset 471276 5b5e620e33ee59c1820d1bef687f015d63c2185c
parent 471275 44883c246d62061847e07dcba4fd88f55eb45362
child 471277 57c8a2867732fa7281ae624eb5b49638f48be54d
push id9348
push userryanvm@gmail.com
push dateMon, 11 Jun 2018 13:44:39 +0000
treeherdermozilla-beta@5b5e620e33ee [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau, RyanVM
bugs1453093
milestone61.0
Bug 1453093 - Move accessible actor instantiation and panel tab highlighting into tool startup component. r=ochameau, a=RyanVM 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/toolbox-controller.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.startTimer(A11Y_SERVICE_DURATION);
     } else {
       this._telemetry.stopTimer(A11Y_SERVICE_DURATION);
     }
   },
 
   selectAccessible(accessibleFront) {
-    this.postContentMessage("selectAccessible", this._walker, accessibleFront);
+    this.postContentMessage("selectAccessible", this.walker, accessibleFront);
   },
 
   selectAccessibleForNode(nodeFront, reason) {
     if (reason) {
       this._telemetry.logKeyedScalar(
         "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";
   },
@@ -238,23 +233,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,
@@ -130,39 +128,29 @@ 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 }) {
-  let { enabled, canBeDisabled, canBeEnabled } = accessibility;
-  toggleHighlightTool(enabled);
+  const { enabled, canBeDisabled, canBeEnabled } = accessibility;
   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 + "'");
 
   let tab = await addTab(url);
   let panel = await initAccessibilityPanel(tab);
   let win = panel.panelWin;
   let doc = win.document;
   let 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");
@@ -105,22 +132,23 @@ 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) {
-  let { doc, win, panel } = env;
+  const { 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);
+  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",
@@ -443,17 +444,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/toolbox-controller.js
+++ b/devtools/client/framework/components/toolbox-controller.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) {
     let { highlightedTools } = this.state;
     highlightedTools.add(highlightedTool);
     this.setState({ highlightedTools });
   }
 
   unhighlightTool(id) {
     let { highlightedTools } = this.state;
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -112,16 +112,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.
@@ -1443,16 +1445,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
@@ -2086,16 +2092,29 @@ Toolbox.prototype = {
     const index = definitions.findIndex(({id}) => id === this.currentToolId);
     let 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;
@@ -2596,16 +2615,35 @@ Toolbox.prototype = {
       let 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);
@@ -2632,16 +2670,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
@@ -2841,16 +2881,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.