Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 26 Jan 2015 16:34:12 -0500
changeset 225865 38e4719e71af140cb95f04f7165d43a34c35a30d
parent 225819 a6f037b538edf7bf154129fe124e81b920eb805f (current diff)
parent 225864 0f87f9d002c11ff8b42654664d9a35911ff93017 (diff)
child 225866 bd91233cd18e59179bd70963f2add425f28d470f
child 225867 45d4cd8bbc04cfe6cdd0107977222d3736b813a9
child 225896 c1cb74f492e16bf002bda6196b70fc334d087d8e
child 225981 4f669be8049b04249fdb3572cc6321389cd6e64c
push id28176
push userryanvm@gmail.com
push dateMon, 26 Jan 2015 21:48:45 +0000
treeherdermozilla-central@38e4719e71af [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone38.0a1
first release with
nightly linux32
38e4719e71af / 38.0a1 / 20150127030230 / files
nightly linux64
38e4719e71af / 38.0a1 / 20150127030230 / files
nightly mac
38e4719e71af / 38.0a1 / 20150127030230 / files
nightly win32
38e4719e71af / 38.0a1 / 20150127030230 / files
nightly win64
38e4719e71af / 38.0a1 / 20150127030230 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c. a=merge
--- a/b2g/chrome/content/devtools/debugger.js
+++ b/b2g/chrome/content/devtools/debugger.js
@@ -36,17 +36,20 @@ let RemoteDebugger = {
     shell.sendChromeEvent({
       "type": "remote-debugger-prompt"
     });
 
     while(!this._promptDone) {
       Services.tm.currentThread.processNextEvent(true);
     }
 
-    return this._promptAnswer;
+    if (this._promptAnswer) {
+      return DebuggerServer.AuthenticationResult.ALLOW;
+    }
+    return DebuggerServer.AuthenticationResult.DENY;
   },
 
   _listen: function() {
     if (this._listening) {
       return;
     }
 
     this.handleEvent = this.handleEvent.bind(this);
@@ -136,19 +139,22 @@ let USBRemoteDebugger = {
     RemoteDebugger.initServer();
 
     let portOrPath =
       Services.prefs.getCharPref("devtools.debugger.unix-domain-socket") ||
       "/data/local/debugger-socket";
 
     try {
       debug("Starting USB debugger on " + portOrPath);
+      let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
+      let authenticator = new AuthenticatorType.Server();
+      authenticator.allowConnection = RemoteDebugger.prompt;
       this._listener = DebuggerServer.createListener();
       this._listener.portOrPath = portOrPath;
-      this._listener.allowConnection = RemoteDebugger.prompt;
+      this._listener.authenticator = authenticator;
       this._listener.open();
       // Temporary event, until bug 942756 lands and offers a way to know
       // when the server is up and running.
       Services.obs.notifyObservers(null, "debugger-server-started", null);
     } catch (e) {
       debug("Unable to start USB debugger server: " + e);
     }
   },
@@ -174,19 +180,22 @@ let WiFiRemoteDebugger = {
     if (this._listener) {
       return;
     }
 
     RemoteDebugger.initServer();
 
     try {
       debug("Starting WiFi debugger");
+      let AuthenticatorType = DebuggerServer.Authenticators.get("OOB_CERT");
+      let authenticator = new AuthenticatorType.Server();
+      authenticator.allowConnection = RemoteDebugger.prompt;
       this._listener = DebuggerServer.createListener();
       this._listener.portOrPath = -1 /* any available port */;
-      this._listener.allowConnection = RemoteDebugger.prompt;
+      this._listener.authenticator = authenticator;
       this._listener.discoverable = true;
       this._listener.encryption = true;
       this._listener.open();
       let port = this._listener.port;
       debug("Started WiFi debugger on " + port);
     } catch (e) {
       debug("Unable to start WiFi debugger server: " + e);
     }
--- a/browser/base/content/browser-gestureSupport.js
+++ b/browser/base/content/browser-gestureSupport.js
@@ -1,14 +1,12 @@
 # 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/.
 
-Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
-
 // Simple gestures support
 //
 // As per bug #412486, web content must not be allowed to receive any
 // simple gesture events.  Multi-touch gesture APIs are in their
 // infancy and we do NOT want to be forced into supporting an API that
 // will probably have to change in the future.  (The current Mac OS X
 // API is undocumented and was reverse-engineered.)  Until support is
 // implemented in the event dispatcher to keep these events as
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1187,16 +1187,20 @@ toolbarpaletteitem[place="palette"][hidd
 
 #customization-palette .toolbarpaletteitem-box {
   -moz-box-pack: center;
   -moz-box-flex: 1;
   width: 10em;
   max-width: 10em;
 }
 
+#main-window[customizing=true] #PanelUI-update-status {
+  display: none;
+}
+
 /* UI Tour */
 
 @keyframes uitour-wobble {
   from {
     transform: rotate(0deg) translateX(3px) rotate(0deg);
   }
   50% {
     transform: rotate(360deg) translateX(3px) rotate(-360deg);
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1267,17 +1267,17 @@
           let installCallback = {
             onSuccess: function(engine) {
               event.target.hidePopup();
               BrowserSearch.searchBar.openSuggestionsPanel();
             }
           }
           Services.search.addEngine(target.getAttribute("uri"),
                                     Ci.nsISearchEngine.DATA_XML,
-                                    target.getAttribute("src"), false,
+                                    target.getAttribute("image"), false,
                                     installCallback);
         }
       ]]></handler>
 
       <handler event="popuphiding"><![CDATA[
         this._isHiding = true;
         setTimeout(() => {
           this._isHiding = false;
--- a/browser/components/loop/README.txt
+++ b/browser/components/loop/README.txt
@@ -7,40 +7,16 @@ modern browser that supports WebRTC.
 
 The standalone client is a set of web pages intended to be hosted on a
 standalone server referenced by the loop-server.
 
 The standalone client exists in standalone/ but shares items
 (from content/shared/) with the desktop implementation. See the README.md
 file in the standalone/ directory for how to run the server locally.
 
-
-Hacking
-=======
-Please be sure to execute
-
-  browser/components/loop/run-all-loop-tests.sh
-
-from the top level before requesting review on a patch.
-
-
-Functional Tests
-================
-These are currently a work in progress, but it's already possible to run a test
-if you have a [loop-server](https://github.com/mozilla-services/loop-server)
-install that is properly configured.  From the top-level gecko directory,
-execute:
-
-  export LOOP_SERVER=/Users/larry/src/loop-server
-  ./mach marionette-test browser/components/loop/test/functional/manifest.ini
-
-Once the automation is complete, we'll include this in run-all-loop-tests.sh
-as well.
-
-
 Working with React JSX files
 ============================
 
 Our views use [React](http://facebook.github.io/react/) written in JSX files
 and transpiled to JS before we commit. You need to install the JSX compiler
 using npm in order to compile the .jsx files into regular .js ones:
 
     npm install -g react-tools
@@ -49,8 +25,53 @@ Once installed, run build-jsx with the -
 browser/components/loop, eg.:
 
     cd browser/components/loop
     ./build-jsx --watch
 
 build-jsx can also be do a one-time compile pass instead of watching if
 the --watch argument is omitted.  Be sure to commit any transpiled files
 at the same time as changes to their sources.
+
+
+Hacking
+=======
+Please be sure to execute
+
+  browser/components/loop/run-all-loop-tests.sh
+
+from the top level before requesting review on a patch.
+
+
+Front-End Unit Tests
+====================
+The unit tests for Loop reside in three directories:
+
+- test/desktop-local
+- test/shared
+- test/standalone
+
+You can run these as part of the run-all-loop-tests.sh command above, or you can run these individually in Firefox. To run them individually, start the standalone client (see standalone/README.md) and load:
+
+  http://localhost:3000/test/
+
+
+Functional Tests
+================
+These are currently a work in progress, but it's already possible to run a test
+if you have a [loop-server](https://github.com/mozilla-services/loop-server)
+install that is properly configured.  From the top-level gecko directory,
+execute:
+
+  export LOOP_SERVER=/Users/larry/src/loop-server
+  ./mach marionette-test browser/components/loop/test/functional/manifest.ini
+
+Once the automation is complete, we'll include this in run-all-loop-tests.sh
+as well.
+
+
+UI-Showcase
+===========
+This is a tool giving the layouts for all the frontend views of Loop, allowing debugging and testing of css layouts and local component behavior.
+
+To access it, start the standalone client (see standalone/README.md) and load:
+
+  http://localhost:3000/ui/
--- a/browser/components/preferences/in-content/search.xul
+++ b/browser/components/preferences/in-content/search.xul
@@ -40,19 +40,20 @@
     <groupbox id="oneClickSearchProvidersGroup" data-category="paneSearch">
       <caption label="&oneClickSearchEngines.label;"/>
       <label>&chooseWhichOneToDisplay.label;</label>
 
       <tree id="engineList" flex="1" rows="8" hidecolumnpicker="true" editable="true"
             seltype="single">
         <treechildren id="engineChildren" flex="1"/>
         <treecols>
-          <treecol id="engineShown" type="checkbox" editable="true"/>
-          <treecol id="engineName" flex="4" label="&engineNameColumn.label;"/>
-          <treecol id="engineKeyword" flex="1" label="&engineKeywordColumn.label;" editable="true"/>
+          <treecol id="engineShown" type="checkbox" editable="true" sortable="false"/>
+          <treecol id="engineName" flex="4" label="&engineNameColumn.label;" sortable="false"/>
+          <treecol id="engineKeyword" flex="1" label="&engineKeywordColumn.label;" editable="true"
+                   sortable="false"/>
         </treecols>
       </tree>
 
       <hbox>
         <button id="restoreDefaultSearchEngines"
                 label="&restoreDefaultSearchEngines.label;"
                 accesskey="&restoreDefaultSearchEngines.accesskey;"
                 />
--- a/browser/components/preferences/search.xul
+++ b/browser/components/preferences/search.xul
@@ -54,19 +54,21 @@
       <label>&chooseWhichOneToDisplay.label;</label>
 
       <tree id="engineList" flex="1" rows="8" hidecolumnpicker="true" editable="true"
             seltype="single" onselect="gSearchPane.onTreeSelect();"
             onkeypress="gSearchPane.onTreeKeyPress(event);">
         <treechildren id="engineChildren" flex="1"
                       ondragstart="onDragEngineStart(event);"/>
         <treecols>
-          <treecol id="engineShown" type="checkbox" style="min-width: 26px;" editable="true"/>
-          <treecol id="engineName" flex="4" label="&engineNameColumn.label;"/>
-          <treecol id="engineKeyword" flex="1" label="&engineKeywordColumn.label;" editable="true"/>
+          <treecol id="engineShown" type="checkbox" style="min-width: 26px;" editable="true"
+                   sortable="false"/>
+          <treecol id="engineName" flex="4" label="&engineNameColumn.label;" sortable="false"/>
+          <treecol id="engineKeyword" flex="1" label="&engineKeywordColumn.label;" editable="true"
+                   sortable="false"/>
         </treecols>
       </tree>
 
       <hbox>
         <button id="restoreDefaultSearchEngines"
                 label="&restoreDefaultSearchEngines.label;"
                 accesskey="&restoreDefaultSearchEngines.accesskey;"
                 oncommand="gSearchPane.onRestoreDefaults();"/>
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -1,10 +1,9 @@
 [DEFAULT]
-skip-if = e10s # Bug ?????? - devtools tests disabled with e10s
 subsuite = devtools
 support-files =
   browser_layoutHelpers.html
   browser_layoutHelpers-getBoxQuads.html
   browser_layoutHelpers_iframe.html
   browser_templater_basic.html
   browser_toolbar_basic.html
   browser_toolbar_webconsole_errors_count.html
@@ -45,31 +44,37 @@ support-files =
 [browser_graphs-10b.js]
 [browser_graphs-11a.js]
 [browser_graphs-11b.js]
 [browser_graphs-12.js]
 [browser_graphs-13.js]
 [browser_graphs-14.js]
 [browser_inplace-editor.js]
 [browser_layoutHelpers.js]
+skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_layoutHelpers-getBoxQuads.js]
+skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_num-l10n.js]
 [browser_observableobject.js]
+[browser_options-view-01.js]
 [browser_outputparser.js]
+skip-if = e10s # Test intermittently fails with e10s. Bug 1124162.
 [browser_prefs.js]
 [browser_require_basic.js]
 [browser_spectrum.js]
 [browser_theme.js]
 [browser_tableWidget_basic.js]
 [browser_tableWidget_keyboard_interaction.js]
 [browser_tableWidget_mouse_interaction.js]
 skip-if = buildapp == 'mulet'
 [browser_telemetry_button_eyedropper.js]
 [browser_telemetry_button_paintflashing.js]
+skip-if = e10s # Bug 937167 - e10s paintflashing
 [browser_telemetry_button_responsive.js]
+skip-if = e10s # Bug 1067145 - e10s responsiveview
 [browser_telemetry_button_scratchpad.js]
 [browser_telemetry_button_tilt.js]
 skip-if = e10s # Bug 1086492 - Disable tilt for e10s
                # Bug 937166 - Make tilt work in E10S mode
 [browser_telemetry_sidebar.js]
 [browser_telemetry_toolbox.js]
 [browser_telemetry_toolboxtabs_canvasdebugger.js]
 [browser_telemetry_toolboxtabs_inspector.js]
@@ -81,13 +86,12 @@ skip-if = e10s # Bug 1086492 - Disable t
 [browser_telemetry_toolboxtabs_storage.js]
 [browser_telemetry_toolboxtabs_styleeditor.js]
 [browser_telemetry_toolboxtabs_webaudioeditor.js]
 [browser_telemetry_toolboxtabs_webconsole.js]
 [browser_templater_basic.js]
 [browser_toolbar_basic.js]
 [browser_toolbar_tooltip.js]
 [browser_toolbar_webconsole_errors_count.js]
-skip-if = buildapp == 'mulet'
+skip-if = buildapp == 'mulet' || e10s # The developertoolbar error count isn't correct with e10s
 [browser_treeWidget_basic.js]
 [browser_treeWidget_keyboard_interaction.js]
 [browser_treeWidget_mouse_interaction.js]
-[browser_options-view-01.js]
--- a/browser/devtools/shared/test/browser_css_color.js
+++ b/browser/devtools/shared/test/browser_css_color.js
@@ -1,90 +1,78 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
-
+const TEST_URI = "data:text/html;charset=utf-8,browser_css_color.js";
+let {colorUtils} = devtools.require("devtools/css-color");
 let origColorUnit;
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-let {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
-let {colorUtils} = devtools.require("devtools/css-color");
 
-function test() {
-  waitForExplicitFinish();
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
+  origColorUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
 
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function onload() {
-    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = "data:text/html;charset=utf-8,browser_css_color.js";
-}
+  info("Creating a test canvas element to test colors");
+  let canvas = createTestCanvas(doc);
+  info("Starting the test");
+  testColorUtils(canvas);
 
-function init() {
-  origColorUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
-  createDocument();
-}
+  host.destroy();
+  gBrowser.removeCurrentTab();
+});
 
-function createDocument()
-{
-  let doc = content.document;
-
+function createTestCanvas(doc) {
   let canvas = doc.createElement("canvas");
   canvas.width = canvas.height = 10;
   doc.body.appendChild(canvas);
-
-  testColorUtils();
+  return canvas;
 }
 
-function testColorUtils() {
+function testColorUtils(canvas) {
   let data = getTestData();
 
   for (let {authored, name, hex, hsl, rgb} of data) {
     let color = new colorUtils.CssColor(authored);
 
     // Check all values.
     info("Checking values for " + authored);
     is(color.name, name, "color.name === name");
     is(color.hex, hex, "color.hex === hex");
     is(color.hsl, hsl, "color.hsl === hsl");
     is(color.rgb, rgb, "color.rgb === rgb");
 
     testToString(color, name, hex, hsl, rgb);
-    testColorMatch(name, hex, hsl, rgb, color.rgba);
+    testColorMatch(name, hex, hsl, rgb, color.rgba, canvas);
   }
+
   testProcessCSSString();
   testSetAlpha();
-  finishUp();
 }
 
 function testToString(color, name, hex, hsl, rgb) {
   switchColorUnit(colorUtils.CssColor.COLORUNIT.name);
   is(color.toString(), name, "toString() with authored type");
 
   switchColorUnit(colorUtils.CssColor.COLORUNIT.hex);
   is(color.toString(), hex, "toString() with hex type");
 
   switchColorUnit(colorUtils.CssColor.COLORUNIT.hsl);
   is(color.toString(), hsl, "toString() with hsl type");
 
   switchColorUnit(colorUtils.CssColor.COLORUNIT.rgb);
   is(color.toString(), rgb, "toString() with rgb type");
-
 }
 
 function switchColorUnit(unit) {
   Services.prefs.setCharPref(COLOR_UNIT_PREF, unit);
 }
 
-function testColorMatch(name, hex, hsl, rgb, rgba) {
+function testColorMatch(name, hex, hsl, rgb, rgba, canvas) {
   let target;
-
-  let canvas = content.document.querySelector("canvas");
   let ctx = canvas.getContext("2d");
 
   let clearCanvas = function() {
     canvas.width = 1;
   };
   let setColor = function(aColor) {
     ctx.fillStyle = aColor;
     ctx.fillRect(0, 0, 1, 1);
@@ -157,22 +145,16 @@ function testSetAlpha() {
     ok(false, "Should fail when passing in an invalid color.");
   } catch (e) {
     ok(true, "Fails when setAlpha receives an invalid color.");
   }
 
   is(colorUtils.setAlpha("#fff"), "rgba(255, 255, 255, 1)", "sets alpha to 1 if invalid.");
 }
 
-function finishUp() {
-  Services = colorUtils = Loader = null;
-  gBrowser.removeCurrentTab();
-  finish();
-}
-
 function getTestData() {
   return [
     {authored: "aliceblue", name: "aliceblue", hex: "#F0F8FF", hsl: "hsl(208, 100%, 97%)", rgb: "rgb(240, 248, 255)"},
     {authored: "antiquewhite", name: "antiquewhite", hex: "#FAEBD7", hsl: "hsl(34, 78%, 91%)", rgb: "rgb(250, 235, 215)"},
     {authored: "aqua", name: "aqua", hex: "#0FF", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
     {authored: "aquamarine", name: "aquamarine", hex: "#7FFFD4", hsl: "hsl(160, 100%, 75%)", rgb: "rgb(127, 255, 212)"},
     {authored: "azure", name: "azure", hex: "#F0FFFF", hsl: "hsl(180, 100%, 97%)", rgb: "rgb(240, 255, 255)"},
     {authored: "beige", name: "beige", hex: "#F5F5DC", hsl: "hsl(60, 56%, 91%)", rgb: "rgb(245, 245, 220)"},
--- a/browser/devtools/shared/test/browser_cubic-bezier-01.js
+++ b/browser/devtools/shared/test/browser_cubic-bezier-01.js
@@ -4,21 +4,22 @@
 
 "use strict";
 
 // Tests that the CubicBezierWidget generates content in a given parent node
 
 const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
 const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget");
 
-let test = Task.async(function*() {
-  yield promiseTab(TEST_URI);
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
   info("Checking that the markup is created in the parent");
-  let container = content.document.querySelector("#container");
+  let container = doc.querySelector("#container");
   let w = new CubicBezierWidget(container);
 
   ok(container.querySelector(".coordinate-plane"),
     "The coordinate plane has been added");
   let buttons = container.querySelectorAll("button");
   is(buttons.length, 2,
     "The 2 control points have been added");
   is(buttons[0].className, "control-point");
@@ -26,11 +27,11 @@ let test = Task.async(function*() {
   is(buttons[1].className, "control-point");
   is(buttons[1].id, "P2");
   ok(container.querySelector("canvas"), "The curve canvas has been added");
 
   info("Destroying the widget");
   w.destroy();
   is(container.children.length, 0, "All nodes have been removed");
 
+  host.destroy();
   gBrowser.removeCurrentTab();
-  finish();
 });
--- a/browser/devtools/shared/test/browser_cubic-bezier-02.js
+++ b/browser/devtools/shared/test/browser_cubic-bezier-02.js
@@ -5,66 +5,63 @@
 "use strict";
 
 // Tests the CubicBezierWidget events
 
 const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
 const {CubicBezierWidget, PREDEFINED} =
   devtools.require("devtools/shared/widgets/CubicBezierWidget");
 
-let test = Task.async(function*() {
-  yield promiseTab(TEST_URI);
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
-  let container = content.document.querySelector("#container");
+  let container = doc.querySelector("#container");
   let w = new CubicBezierWidget(container, PREDEFINED.linear);
 
-  yield pointsCanBeDragged(w);
-  yield curveCanBeClicked(w);
-  yield pointsCanBeMovedWithKeyboard(w);
+  yield pointsCanBeDragged(w, win, doc);
+  yield curveCanBeClicked(w, win, doc);
+  yield pointsCanBeMovedWithKeyboard(w, win, doc);
 
   w.destroy();
+  host.destroy();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
-function* pointsCanBeDragged(widget) {
+function* pointsCanBeDragged(widget, win, doc) {
   info("Checking that the control points can be dragged with the mouse");
 
   info("Listening for the update event");
   let onUpdated = widget.once("updated");
 
   info("Generating a mousedown/move/up on P1");
   widget._onPointMouseDown({target: widget.p1});
-  EventUtils.synthesizeMouse(content.document.documentElement, 0, 100,
-    {type: "mousemove"}, content.window);
-  EventUtils.synthesizeMouse(content.document.documentElement, 0, 100,
-    {type: "mouseup"}, content.window);
+  doc.onmousemove({pageX: 0, pageY: 100});
+  doc.onmouseup();
 
   let bezier = yield onUpdated;
   ok(true, "The widget fired the updated event");
   ok(bezier, "The updated event contains a bezier argument");
   is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
 
   info("Listening for the update event");
   onUpdated = widget.once("updated");
 
   info("Generating a mousedown/move/up on P2");
   widget._onPointMouseDown({target: widget.p2});
-  EventUtils.synthesizeMouse(content.document.documentElement, 200, 300,
-    {type: "mousemove"}, content.window);
-  EventUtils.synthesizeMouse(content.document.documentElement, 200, 300,
-    {type: "mouseup"}, content.window);
+  doc.onmousemove({pageX: 200, pageY: 300});
+  doc.onmouseup();
 
   bezier = yield onUpdated;
   is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
   is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
 }
 
-function* curveCanBeClicked(widget) {
+function* curveCanBeClicked(widget, win, doc) {
   info("Checking that clicking on the curve moves the closest control point");
 
   info("Listening for the update event");
   let onUpdated = widget.once("updated");
 
   info("Click close to P1");
   widget._onCurveClick({pageX: 50, pageY: 150});
 
@@ -83,17 +80,17 @@ function* curveCanBeClicked(widget) {
 
   bezier = yield onUpdated;
   is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct");
   is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
   is(bezier.P1[0], 0.25, "P1 time coordinate remained unchanged");
   is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged");
 }
 
-function* pointsCanBeMovedWithKeyboard(widget) {
+function* pointsCanBeMovedWithKeyboard(widget, win, doc) {
   info("Checking that points respond to keyboard events");
 
   info("Moving P1 to the left");
   let onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 37));
   let bezier = yield onUpdated;
   is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
--- a/browser/devtools/shared/test/browser_cubic-bezier-03.js
+++ b/browser/devtools/shared/test/browser_cubic-bezier-03.js
@@ -5,28 +5,29 @@
 "use strict";
 
 // Tests that coordinates can be changed programatically in the CubicBezierWidget
 
 const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
 const {CubicBezierWidget, PREDEFINED} =
   devtools.require("devtools/shared/widgets/CubicBezierWidget");
 
-let test = Task.async(function*() {
-  yield promiseTab(TEST_URI);
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
-  let container = content.document.querySelector("#container");
+  let container = doc.querySelector("#container");
   let w = new CubicBezierWidget(container, PREDEFINED.linear);
 
   yield coordinatesCanBeChangedByProvidingAnArray(w);
   yield coordinatesCanBeChangedByProvidingAValue(w);
 
   w.destroy();
+  host.destroy();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* coordinatesCanBeChangedByProvidingAnArray(widget) {
   info("Listening for the update event");
   let onUpdated = widget.once("updated");
 
   info("Setting new coordinates");
   widget.coordinates = [0,1,1,0];
--- a/browser/devtools/shared/test/browser_flame-graph-01.js
+++ b/browser/devtools/shared/test/browser_flame-graph-01.js
@@ -1,23 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that flame graph widget works properly.
 
 let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
 
   let graph = new FlameGraph(doc.body);
 
--- a/browser/devtools/shared/test/browser_flame-graph-02.js
+++ b/browser/devtools/shared/test/browser_flame-graph-02.js
@@ -1,23 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that flame graph widgets may have a fixed width or height.
 
 let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
 
   let graph = new FlameGraph(doc.body);
   graph.fixedWidth = 200;
--- a/browser/devtools/shared/test/browser_flame-graph-03a.js
+++ b/browser/devtools/shared/test/browser_flame-graph-03a.js
@@ -4,25 +4,22 @@
 // Tests that selections in the flame graph widget work properly.
 
 let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
 let TEST_BOUNDS = { startTime: 0, endTime: 150 };
 let TEST_WIDTH = 200;
 let TEST_HEIGHT = 100;
 
 let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
 
   let graph = new FlameGraph(doc.body, 1);
   graph.fixedWidth = TEST_WIDTH;
--- a/browser/devtools/shared/test/browser_flame-graph-03b.js
+++ b/browser/devtools/shared/test/browser_flame-graph-03b.js
@@ -5,25 +5,22 @@
 
 let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
 let TEST_BOUNDS = { startTime: 0, endTime: 150 };
 let TEST_WIDTH = 200;
 let TEST_HEIGHT = 100;
 let TEST_DPI_DENSITIY = 2;
 
 let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
 
   let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
   graph.fixedWidth = TEST_WIDTH;
--- a/browser/devtools/shared/test/browser_flame-graph-04.js
+++ b/browser/devtools/shared/test/browser_flame-graph-04.js
@@ -3,27 +3,24 @@
 
 // Tests that text metrics in the flame graph widget work properly.
 
 let HTML_NS = "http://www.w3.org/1999/xhtml";
 let FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 8; // px
 let FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "sans-serif";
 let {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
 let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
 let L10N = new ViewHelpers.L10N();
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new FlameGraph(doc.body, 1);
   yield graph.ready();
 
   testGraph(graph);
--- a/browser/devtools/shared/test/browser_flame-graph-utils-01.js
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-01.js
@@ -1,21 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that text metrics and data conversion from profiler samples
 // widget work properly in the flame graph.
 
 let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA);
 
   ok(out, "Some data was outputted properly");
   is(out.length, 10, "The outputted length is correct.");
 
--- a/browser/devtools/shared/test/browser_flame-graph-utils-02.js
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-02.js
@@ -1,20 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests consecutive duplicate frames are removed from the flame graph data.
 
 let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, {
     flattenRecursion: true
   });
 
   ok(out, "Some data was outputted properly");
--- a/browser/devtools/shared/test/browser_flame-graph-utils-03.js
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-03.js
@@ -1,21 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if platform frames are removed from the flame graph data.
 
 let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
 let {FrameNode} = devtools.require("devtools/profiler/tree-model");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, {
     filterFrames: FrameNode.isContent
   });
 
   ok(out, "Some data was outputted properly");
--- a/browser/devtools/shared/test/browser_flame-graph-utils-04.js
+++ b/browser/devtools/shared/test/browser_flame-graph-utils-04.js
@@ -1,21 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if (idle) nodes are added when necessary in the flame graph data.
 
 let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
 let {FrameNode} = devtools.require("devtools/profiler/tree-model");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, {
     flattenRecursion: true,
     filterFrames: FrameNode.isContent,
     showIdleBlocks: "\m/"
   });
--- a/browser/devtools/shared/test/browser_graphs-01.js
+++ b/browser/devtools/shared/test/browser_graphs-01.js
@@ -1,19 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets works properly.
 
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
   finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
--- a/browser/devtools/shared/test/browser_graphs-02.js
+++ b/browser/devtools/shared/test/browser_graphs-02.js
@@ -1,25 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets can properly add data, regions and highlights.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
   yield graph.once("ready");
 
   testDataAndRegions(graph);
--- a/browser/devtools/shared/test/browser_graphs-03.js
+++ b/browser/devtools/shared/test/browser_graphs-03.js
@@ -1,24 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets can handle clients getting/setting the
 // selection or cursor.
 
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
   yield graph.once("ready");
 
   yield testSelection(graph);
--- a/browser/devtools/shared/test/browser_graphs-04.js
+++ b/browser/devtools/shared/test/browser_graphs-04.js
@@ -1,23 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets can correctly compare selections and cursors.
 
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
   yield graph.once("ready");
 
   testGraph(graph);
--- a/browser/devtools/shared/test/browser_graphs-05.js
+++ b/browser/devtools/shared/test/browser_graphs-05.js
@@ -1,25 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets can correctly determine which regions are hovered.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
   yield graph.once("ready");
 
   testGraph(graph);
--- a/browser/devtools/shared/test/browser_graphs-06.js
+++ b/browser/devtools/shared/test/browser_graphs-06.js
@@ -1,25 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if clicking on regions adds a selection spanning that region.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
   yield graph.once("ready");
 
   testGraph(graph);
--- a/browser/devtools/shared/test/browser_graphs-07a.js
+++ b/browser/devtools/shared/test/browser_graphs-07a.js
@@ -1,24 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if selecting, resizing, moving selections and zooming in/out works.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
   yield graph.once("ready");
 
   testGraph(graph);
--- a/browser/devtools/shared/test/browser_graphs-07b.js
+++ b/browser/devtools/shared/test/browser_graphs-07b.js
@@ -1,24 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if selections can't be added via clicking, while not allowed.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
   yield graph.once("ready");
 
   testGraph(graph);
--- a/browser/devtools/shared/test/browser_graphs-08.js
+++ b/browser/devtools/shared/test/browser_graphs-08.js
@@ -1,24 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests if a selection is dropped when clicking outside of it.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
   yield graph.once("ready");
 
   testGraph(graph);
--- a/browser/devtools/shared/test/browser_graphs-09a.js
+++ b/browser/devtools/shared/test/browser_graphs-09a.js
@@ -1,24 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that line graphs properly create the gutter and tooltips.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, { metric: "fps" });
 
   yield testGraph(graph);
 
--- a/browser/devtools/shared/test/browser_graphs-09b.js
+++ b/browser/devtools/shared/test/browser_graphs-09b.js
@@ -1,24 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that line graphs properly use the tooltips configuration properties.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
   graph.withTooltipArrows = false;
   graph.withFixedTooltipPositions = true;
 
--- a/browser/devtools/shared/test/browser_graphs-09c.js
+++ b/browser/devtools/shared/test/browser_graphs-09c.js
@@ -1,24 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that line graphs hide the tooltips when there's no data available.
 
 const TEST_DATA = [];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
 
   yield testGraph(graph);
 
--- a/browser/devtools/shared/test/browser_graphs-09d.js
+++ b/browser/devtools/shared/test/browser_graphs-09d.js
@@ -1,25 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that line graphs hide the 'max' tooltip when the distance between
 // the 'min' and 'max' tooltip is too small.
 
 const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 59.9 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
 
   yield testGraph(graph);
 
--- a/browser/devtools/shared/test/browser_graphs-09e.js
+++ b/browser/devtools/shared/test/browser_graphs-09e.js
@@ -3,25 +3,22 @@
 
 // Tests that line graphs hide the gutter and tooltips when there's no data,
 // but show them when there is.
 
 const NO_DATA = [];
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
 
   yield testGraph(graph);
 
--- a/browser/devtools/shared/test/browser_graphs-09f.js
+++ b/browser/devtools/shared/test/browser_graphs-09f.js
@@ -1,25 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests the constructor options for `min`, `max` and `avg` on displaying the
 // gutter/tooltips and lines.
 
 const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 1 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
 
   yield testGraph(doc.body, { avg: false });
   yield testGraph(doc.body, { min: false });
   yield testGraph(doc.body, { max: false });
--- a/browser/devtools/shared/test/browser_graphs-10a.js
+++ b/browser/devtools/shared/test/browser_graphs-10a.js
@@ -1,24 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graphs properly handle resizing.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost("window");
   doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
 
   let graph = new LineGraphWidget(doc.body, "fps");
   yield graph.once("ready");
--- a/browser/devtools/shared/test/browser_graphs-10b.js
+++ b/browser/devtools/shared/test/browser_graphs-10b.js
@@ -1,25 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graphs aren't refreshed when the owner window resizes but
 // the graph dimensions stay the same.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost("window");
   doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
 
   let graph = new LineGraphWidget(doc.body, "fps");
   graph.fixedWidth = 200;
--- a/browser/devtools/shared/test/browser_graphs-11a.js
+++ b/browser/devtools/shared/test/browser_graphs-11a.js
@@ -1,29 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that bar graph create a legend as expected.
 
 let {BarGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
 const CATEGORIES = [
   { color: "#46afe3", label: "Foo" },
   { color: "#eb5368", label: "Bar" },
   { color: "#70bf53", label: "Baz" }
 ];
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new BarGraphWidget(doc.body);
   yield graph.once("ready");
 
   testGraph(graph);
--- a/browser/devtools/shared/test/browser_graphs-11b.js
+++ b/browser/devtools/shared/test/browser_graphs-11b.js
@@ -1,29 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that bar graph's legend items handle mouseover/mouseout.
 
 let {BarGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
 const CATEGORIES = [
   { color: "#46afe3", label: "Foo" },
   { color: "#eb5368", label: "Bar" },
   { color: "#70bf53", label: "Baz" }
 ];
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
 
   let graph = new BarGraphWidget(doc.body, 1);
   graph.fixedWidth = 200;
--- a/browser/devtools/shared/test/browser_graphs-12.js
+++ b/browser/devtools/shared/test/browser_graphs-12.js
@@ -1,23 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that canvas graphs can have their selection linked.
 
 let {LineGraphWidget, BarGraphWidget, CanvasGraphUtils} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
 
   let first = document.createElement("div");
   first.setAttribute("style", "display: inline-block; width: 100%; height: 50%;");
--- a/browser/devtools/shared/test/browser_graphs-13.js
+++ b/browser/devtools/shared/test/browser_graphs-13.js
@@ -1,23 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets may have a fixed width or height.
 
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
 
   let graph = new LineGraphWidget(doc.body, "fps");
   graph.fixedWidth = 200;
--- a/browser/devtools/shared/test/browser_graphs-14.js
+++ b/browser/devtools/shared/test/browser_graphs-14.js
@@ -1,24 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that graph widgets correctly emit mouse input events.
 
 const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
 let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
-let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
 let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
 
   yield testGraph(graph);
 
--- a/browser/devtools/shared/test/browser_inplace-editor.js
+++ b/browser/devtools/shared/test/browser_inplace-editor.js
@@ -4,119 +4,120 @@
 
 "use strict";
 
 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 let {editableField, getInplaceEditorForSpan: inplaceEditor} = devtools.require("devtools/shared/inplace-editor");
 
 // Test the inplace-editor behavior.
 
-let test = Task.async(function*() {
+add_task(function*() {
   yield promiseTab("data:text/html;charset=utf-8,inline editor tests");
+  let [host, win, doc] = yield createHost();
 
-  yield testReturnCommit();
-  yield testBlurCommit();
-  yield testAdvanceCharCommit();
+  yield testReturnCommit(doc);
+  yield testBlurCommit(doc);
+  yield testAdvanceCharCommit(doc);
 
+  host.destroy();
   gBrowser.removeCurrentTab();
-  finish();
 });
 
-function testReturnCommit() {
+function testReturnCommit(doc) {
   info("Testing that pressing return commits the new value");
   let def = promise.defer();
 
   createInplaceEditorAndClick({
     initial: "explicit initial",
     start: function(editor) {
       is(editor.input.value, "explicit initial", "Explicit initial value should be used.");
       editor.input.value = "Test Value";
       EventUtils.sendKey("return");
     },
     done: onDone("Test Value", true, def)
-  });
+  }, doc);
 
   return def.promise;
 }
 
-function testBlurCommit() {
+function testBlurCommit(doc) {
   info("Testing that bluring the field commits the new value");
   let def = promise.defer();
 
   createInplaceEditorAndClick({
     start: function(editor) {
       is(editor.input.value, "Edit Me!", "textContent of the span used.");
       editor.input.value = "Test Value";
       editor.input.blur();
     },
     done: onDone("Test Value", true, def)
-  });
+  }, doc);
 
   return def.promise;
 }
 
-function testAdvanceCharCommit() {
+function testAdvanceCharCommit(doc) {
   info("Testing that configured advanceChars commit the new value");
   let def = promise.defer();
 
   createInplaceEditorAndClick({
     advanceChars: ":",
     start: function(editor) {
       let input = editor.input;
       for each (let ch in "Test:") {
         EventUtils.sendChar(ch);
       }
     },
     done: onDone("Test", true, def)
-  });
+  }, doc);
 
   return def.promise;
 }
 
-function testEscapeCancel() {
+function testEscapeCancel(doc) {
   info("Testing that escape cancels the new value");
   let def = promise.defer();
 
   createInplaceEditorAndClick({
     initial: "initial text",
     start: function(editor) {
       editor.input.value = "Test Value";
       EventUtils.sendKey("escape");
     },
     done: onDone("initial text", false, def)
-  });
+  }, doc);
 
   return def.promise;
 }
 
 function onDone(value, isCommit, def) {
   return function(actualValue, actualCommit) {
     info("Inplace-editor's done callback executed, checking its state");
     is(actualValue, value, "The value is correct");
     is(actualCommit, isCommit, "The commit boolean is correct");
     def.resolve();
   }
 }
 
-function createInplaceEditorAndClick(options) {
-  clearBody();
-  let span = options.element = createSpan();
+function createInplaceEditorAndClick(options, doc) {
+  clearBody(doc);
+  let span = options.element = createSpan(doc);
 
   info("Creating an inplace-editor field");
   editableField(options);
 
   info("Clicking on the inplace-editor field to turn to edit mode");
   span.click();
 }
 
-function clearBody() {
+function clearBody(doc) {
   info("Clearing the page body");
-  content.document.body.innerHTML = "";
+  doc.body.innerHTML = "";
 }
 
-function createSpan() {
+function createSpan(doc) {
   info("Creating a new span element");
-  let span = content.document.createElement("span");
+  let span = doc.createElement("span");
   span.setAttribute("tabindex", "0");
   span.textContent = "Edit Me!";
-  content.document.body.appendChild(span);
+  doc.body.appendChild(span);
   return span;
 }
--- a/browser/devtools/shared/test/browser_options-view-01.js
+++ b/browser/devtools/shared/test/browser_options-view-01.js
@@ -1,41 +1,49 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that options-view OptionsView responds to events correctly.
 
-let { OptionsView } = devtools.require("devtools/shared/options-view");
-let { Services } = devtools.require("resource://gre/modules/Services.jsm");
+const {OptionsView} = devtools.require("devtools/shared/options-view");
+const {Services} = devtools.require("resource://gre/modules/Services.jsm");
 
 const BRANCH = "devtools.debugger.";
 const BLACK_BOX_PREF = "auto-black-box";
 const PRETTY_PRINT_PREF = "auto-pretty-print";
 
 let originalBlackBox = Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF);
 let originalPrettyPrint = Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF);
 
-let test = Task.async(function*() {
+add_task(function*() {
+  info("Setting a couple of preferences");
   Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, false);
   Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, true);
-  let tab = yield promiseTab(OPTIONS_VIEW_URL);
+
+  info("Opening a test tab and a toolbox host to create the options view in");
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", OPTIONS_VIEW_URL);
+
+  yield testOptionsView(win);
 
-  yield testOptionsView(tab);
+  info("Closing the host and current tab");
+  host.destroy();
   gBrowser.removeCurrentTab();
-  cleanup();
-  finish();
+
+  info("Resetting the preferences");
+  Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, originalBlackBox);
+  Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, originalPrettyPrint);
 });
 
-function* testOptionsView(tab) {
+function* testOptionsView(win) {
   let events = [];
-  let options = createOptionsView(tab);
+  let options = createOptionsView(win);
   yield options.initialize();
 
-  let window = tab._contentWindow;
-  let $ = window.document.querySelector.bind(window.document);
+  let $ = win.document.querySelector.bind(win.document);
 
   options.on("pref-changed", (_, pref) => events.push(pref));
 
   let ppEl = $("menuitem[data-pref='auto-pretty-print']");
   let bbEl = $("menuitem[data-pref='auto-black-box']");
 
   // Test default config
   is(ppEl.getAttribute("checked"), "true", "`true` prefs are checked on start");
@@ -52,56 +60,42 @@ function* testOptionsView(tab) {
   is(bbEl.getAttribute("checked"), "true", "menuitems update when preferences change");
 
   // Tests events are fired when preferences update outside of the menu
   is(events.length, 2, "two 'pref-changed' events fired");
   is(events[0], "auto-pretty-print", "correct pref passed in 'pref-changed' event (auto-pretty-print)");
   is(events[1], "auto-black-box", "correct pref passed in 'pref-changed' event (auto-black-box)");
 
   // Test buttons update when clicked and preferences are updated
-  yield click(options, window, ppEl);
+  yield click(options, win, ppEl);
   is(ppEl.getAttribute("checked"), "true", "menuitems update when clicked");
   is(Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF), true, "preference updated via click");
 
-  yield click(options, window, bbEl);
+  yield click(options, win, bbEl);
   is(bbEl.getAttribute("checked"), "", "menuitems update when clicked");
   is(Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF), false, "preference updated via click");
 
   // Tests events are fired when preferences updated via click
   is(events.length, 4, "two 'pref-changed' events fired");
   is(events[2], "auto-pretty-print", "correct pref passed in 'pref-changed' event (auto-pretty-print)");
   is(events[3], "auto-black-box", "correct pref passed in 'pref-changed' event (auto-black-box)");
 
   yield options.destroy();
 }
 
-function wait(window) {
-  return new Promise(function (resolve, reject) {
-  window.setTimeout(() => resolve, 60000);
-  });
-}
-function createOptionsView (tab) {
+function createOptionsView(win) {
   return new OptionsView({
     branchName: BRANCH,
-    window: tab._contentWindow,
-    menupopup: tab._contentWindow.document.querySelector("#options-menupopup")
+    menupopup: win.document.querySelector("#options-menupopup")
   });
 }
 
-function cleanup () {
-  Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, originalBlackBox);
-  Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, originalPrettyPrint);
-}
-
-function* click (view, win, menuitem) {
+function* click(view, win, menuitem) {
   let opened = view.once("options-shown");
   let closed = view.once("options-hidden");
 
   let button = win.document.querySelector("#options-button");
   EventUtils.synthesizeMouseAtCenter(button, {}, win);
   yield opened;
 
   EventUtils.synthesizeMouseAtCenter(menuitem, {}, win);
   yield closed;
 }
-
-function* openMenu (view, win) {
-}
--- a/browser/devtools/shared/test/browser_outputparser.js
+++ b/browser/devtools/shared/test/browser_outputparser.js
@@ -1,38 +1,35 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
 let {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
 let {OutputParser} = devtools.require("devtools/output-parser");
 
-let parser;
-let doc;
-
-function test() {
-  waitForExplicitFinish();
+add_task(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+});
 
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function onload() {
-    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
-    waitForFocus(init, content);
-    doc = content.document;
-  }, true);
+function* performTest() {
+  let [host, win, doc] = yield createHost("bottom", "data:text/html," +
+    "<h1>browser_outputParser.js</h1><div></div>");
 
-  content.location = "data:text/html,<h1>browser_outputParser.js</h1>" +
-                     "<div></div>";
+  let parser = new OutputParser();
+  testParseCssProperty(doc, parser);
+  testParseCssVar(doc, parser);
+  testParseHTMLAttribute(doc, parser);
+  testParseNonCssHTMLAttribute(doc, parser);
+
+  host.destroy();
 }
 
-function init() {
-  parser = new OutputParser();
-  testParseCssProperty();
-}
-
-function testParseCssProperty() {
+function testParseCssProperty(doc, parser) {
   let frag = parser.parseCssProperty("border", "1px solid red", {
     colorSwatchClass: "test-colorswatch"
   });
 
   let target = doc.querySelector("div");
   ok(target, "captain, we have the div");
   target.appendChild(frag);
 
@@ -48,37 +45,33 @@ function testParseCssProperty() {
   });
   target.appendChild(frag);
   is(target.innerHTML,
      'linear-gradient(to right, <span data-color="#F60"><span style="background-color:#F60" class="test-colorswatch"></span><span class="test-color">#F60</span></span> 10%, ' +
      '<span data-color="#000"><span style="background-color:rgba(0,0,0,1)" class="test-colorswatch"></span><span class="test-color">#000</span></span>)',
      "Gradient CSS property correctly parsed");
 
   target.innerHTML = "";
-
-  testParseCssVar();
 }
 
-function testParseCssVar() {
+function testParseCssVar(doc, parser) {
   let frag = parser.parseCssProperty("color", "var(--some-kind-of-green)", {
     colorSwatchClass: "test-colorswatch"
   });
 
   let target = doc.querySelector("div");
   ok(target, "captain, we have the div");
   target.appendChild(frag);
 
   is(target.innerHTML, "var(--some-kind-of-green)", "CSS property correctly parsed");
 
   target.innerHTML = "";
-
-  testParseHTMLAttribute();
 }
 
-function testParseHTMLAttribute() {
+function testParseHTMLAttribute(doc, parser) {
   let attrib = "color:red; font-size: 12px; background-image: " +
                "url(chrome://branding/content/about-logo.png)";
   let frag = parser.parseHTMLAttribute(attrib, {
     urlClass: "theme-link",
     colorClass: "theme-color"
   });
 
   let target = doc.querySelector("div");
@@ -87,32 +80,23 @@ function testParseHTMLAttribute() {
 
   let expected = 'color:<span data-color="#F00"><span class="theme-color">#F00</span></span>; font-size: 12px; ' +
                  'background-image: url("<a href="chrome://branding/content/about-logo.png" ' +
                  'class="theme-link" ' +
                  'target="_blank">chrome://branding/content/about-logo.png</a>")';
 
   is(target.innerHTML, expected, "HTML Attribute correctly parsed");
   target.innerHTML = "";
-  testParseNonCssHTMLAttribute();
 }
 
-function testParseNonCssHTMLAttribute() {
+function testParseNonCssHTMLAttribute(doc, parser) {
   let attrib = "someclass background someotherclass red";
   let frag = parser.parseHTMLAttribute(attrib);
 
   let target = doc.querySelector("div");
   ok(target, "captain, we have the div");
   target.appendChild(frag);
 
   let expected = 'someclass background someotherclass red';
 
   is(target.innerHTML, expected, "Non-CSS HTML Attribute correctly parsed");
   target.innerHTML = "";
-  finishUp();
 }
-
-
-function finishUp() {
-  Services = Loader = OutputParser = parser = doc = null;
-  gBrowser.removeCurrentTab();
-  finish();
-}
--- a/browser/devtools/shared/test/browser_spectrum.js
+++ b/browser/devtools/shared/test/browser_spectrum.js
@@ -2,109 +2,102 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the spectrum color picker works correctly
 
 const TEST_URI = "chrome://browser/content/devtools/spectrum-frame.xhtml";
 const {Spectrum} = devtools.require("devtools/shared/widgets/Spectrum");
 
-let doc;
+add_task(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+});
 
-function test() {
-  waitForExplicitFinish();
-  addTab(TEST_URI, () => {
-    doc = content.document;
-    startTests();
-  });
+function* performTest() {
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+  yield testCreateAndDestroyShouldAppendAndRemoveElements(doc);
+  yield testPassingAColorAtInitShouldSetThatColor(doc);
+  yield testSettingAndGettingANewColor(doc);
+  yield testChangingColorShouldEmitEvents(doc);
+  yield testSettingColorShoudUpdateTheUI(doc);
+
+  host.destroy();
 }
 
-function endTests() {
-  doc = null;
-  gBrowser.removeCurrentTab();
-  finish();
-}
-
-function startTests() {
-  testCreateAndDestroyShouldAppendAndRemoveElements();
-}
-
-function testCreateAndDestroyShouldAppendAndRemoveElements() {
+function testCreateAndDestroyShouldAppendAndRemoveElements(doc) {
   let containerElement = doc.querySelector("#spectrum");
   ok(containerElement, "We have the root node to append spectrum to");
   is(containerElement.childElementCount, 0, "Root node is empty");
 
   let s = new Spectrum(containerElement, [255, 126, 255, 1]);
   s.show();
   ok(containerElement.childElementCount > 0, "Spectrum has appended elements");
 
   s.destroy();
   is(containerElement.childElementCount, 0, "Destroying spectrum removed all nodes");
-
-  testPassingAColorAtInitShouldSetThatColor();
 }
 
-function testPassingAColorAtInitShouldSetThatColor() {
+function testPassingAColorAtInitShouldSetThatColor(doc) {
   let initRgba = [255, 126, 255, 1];
 
   let s = new Spectrum(doc.querySelector("#spectrum"), initRgba);
   s.show();
 
   let setRgba = s.rgb;
 
   is(initRgba[0], setRgba[0], "Spectrum initialized with the right color");
   is(initRgba[1], setRgba[1], "Spectrum initialized with the right color");
   is(initRgba[2], setRgba[2], "Spectrum initialized with the right color");
   is(initRgba[3], setRgba[3], "Spectrum initialized with the right color");
 
   s.destroy();
-  testSettingAndGettingANewColor();
 }
 
-function testSettingAndGettingANewColor() {
+function testSettingAndGettingANewColor(doc) {
   let s = new Spectrum(doc.querySelector("#spectrum"), [0, 0, 0, 1]);
   s.show();
 
   let colorToSet = [255, 255, 255, 1];
   s.rgb = colorToSet;
   let newColor = s.rgb;
 
   is(colorToSet[0], newColor[0], "Spectrum set with the right color");
   is(colorToSet[1], newColor[1], "Spectrum set with the right color");
   is(colorToSet[2], newColor[2], "Spectrum set with the right color");
   is(colorToSet[3], newColor[3], "Spectrum set with the right color");
 
   s.destroy();
-  testChangingColorShouldEmitEvents();
 }
 
-function testChangingColorShouldEmitEvents() {
-  let s = new Spectrum(doc.querySelector("#spectrum"), [255, 255, 255, 1]);
-  s.show();
-
-  s.once("changed", (event, rgba, color) => {
-    EventUtils.sendMouseEvent({type: "mouseup"}, s.dragger, doc.defaultView);
+function testChangingColorShouldEmitEvents(doc) {
+  return new Promise(resolve => {
+    let s = new Spectrum(doc.querySelector("#spectrum"), [255, 255, 255, 1]);
+    s.show();
 
-    ok(true, "Changed event was emitted on color change");
-    is(rgba[0], 128, "New color is correct");
-    is(rgba[1], 64, "New color is correct");
-    is(rgba[2], 64, "New color is correct");
-    is(rgba[3], 1, "New color is correct");
-    is("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " + rgba[3] + ")", color, "RGBA and css color correspond");
+    s.once("changed", (event, rgba, color) => {
+      ok(true, "Changed event was emitted on color change");
+      is(rgba[0], 128, "New color is correct");
+      is(rgba[1], 64, "New color is correct");
+      is(rgba[2], 64, "New color is correct");
+      is(rgba[3], 1, "New color is correct");
+      is("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " + rgba[3] + ")", color, "RGBA and css color correspond");
 
-    s.destroy();
-    testSettingColorShoudUpdateTheUI();
-  });
+      s.destroy();
+      resolve();
+    });
 
-  executeSoon(() => {
-    EventUtils.synthesizeMouse(s.dragger, s.dragger.offsetWidth/2, s.dragger.offsetHeight/2, {}, content);
+    // Simulate a drag move event by calling the handler directly.
+    s.onDraggerMove(s.dragger.offsetWidth/2, s.dragger.offsetHeight/2);
   });
 }
 
-function testSettingColorShoudUpdateTheUI() {
+function testSettingColorShoudUpdateTheUI(doc) {
   let s = new Spectrum(doc.querySelector("#spectrum"), [255, 255, 255, 1]);
   s.show();
   let dragHelperOriginalPos = [s.dragHelper.style.top, s.dragHelper.style.left];
   let alphaHelperOriginalPos = s.alphaSliderHelper.style.left;
 
   s.rgb = [50, 240, 234, .2];
   s.updateUI();
 
@@ -113,10 +106,9 @@ function testSettingColorShoudUpdateTheU
   ok(s.dragHelper.style.left !== dragHelperOriginalPos[1], "Drag helper has moved");
 
   s.rgb = [240, 32, 124, 0];
   s.updateUI();
   is(s.alphaSliderHelper.style.left, - (s.alphaSliderHelper.offsetWidth/2) + "px",
     "Alpha range UI has been updated again");
 
   s.destroy();
-  executeSoon(endTests);
 }
--- a/browser/devtools/shared/test/browser_telemetry_button_eyedropper.js
+++ b/browser/devtools/shared/test/browser_telemetry_button_eyedropper.js
@@ -1,110 +1,57 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
+const TEST_URI = "data:text/html;charset=utf-8," +
+  "<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
 
-let promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+let {EyedropperManager} = require("devtools/eyedropper/eyedropper");
 
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-let { EyedropperManager } = require("devtools/eyedropper/eyedropper");
-
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  info("inspector opened");
 
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
+  info("testing the eyedropper button");
+  testButton(toolbox, Telemetry);
+
+  stopRecordingTelemetryLogs(Telemetry);
+  yield gDevTools.closeToolbox(target);
+  gBrowser.removeCurrentTab();
+});
 
-      this.telemetryInfo[histogramId].push(value);
-    }
-  };
+function testButton(toolbox, Telemetry) {
+  let button = toolbox.doc.querySelector("#command-button-eyedropper");
+  ok(button, "Captain, we have the eyedropper button");
 
-  testButton("command-button-eyedropper");
+  info("clicking the button to open the eyedropper");
+  button.click();
+
+  checkResults("_EYEDROPPER_", Telemetry);
 }
 
-function testButton(id) {
-  info("Testing " + id);
-
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
-    info("inspector opened");
-
-    let button = toolbox.doc.querySelector("#" + id);
-    ok(button, "Captain, we have the button");
-
-    // open the eyedropper
-    button.click();
-
-    checkResults("_EYEDROPPER_");
-  }).then(null, console.error);
-}
-
-function clickButton(node, clicks) {
-  for (let i = 0; i < clicks; i++) {
-    info("Clicking button " + node.id);
-    node.click();
-  }
-}
-
-function checkResults(histIdFocus) {
+function checkResults(histIdFocus, Telemetry) {
   let result = Telemetry.prototype.telemetryInfo;
 
   for (let [histId, value] of Iterator(result)) {
     if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
         !histId.contains(histIdFocus)) {
       // Inspector stats are tested in
       // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
       // because we only open the inspector once for this test.
       continue;
     }
 
     if (histId.endsWith("OPENED_PER_USER_FLAG")) {
       ok(value.length === 1 && value[0] === true,
          "Per user value " + histId + " has a single value of true");
     } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length == 1, histId + " has one entry");
+      is(value.length, 1, histId + " has one entry");
 
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
+      let okay = value.every(element => element === true);
       ok(okay, "All " + histId + " entries are === true");
     }
   }
-
-  finishUp();
 }
-
-function finishUp() {
-  gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
--- a/browser/devtools/shared/test/browser_telemetry_button_paintflashing.js
+++ b/browser/devtools/shared/test/browser_telemetry_button_paintflashing.js
@@ -1,81 +1,64 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_button_paintflashing.js</p>";
+const TEST_URI = "data:text/html;charset=utf-8," +
+  "<p>browser_telemetry_button_paintflashing.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  };
-
-  testButton("command-button-paintflashing");
-}
-
-function testButton(id) {
-  info("Testing " + id);
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
   let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  info("inspector opened");
 
-  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
-    info("inspector opened");
+  info("testing the paintflashing button");
+  yield testButton(toolbox, Telemetry);
 
-    let button = toolbox.doc.querySelector("#" + id);
-    ok(button, "Captain, we have the button");
+  stopRecordingTelemetryLogs(Telemetry);
+  yield gDevTools.closeToolbox(target);
+  gBrowser.removeCurrentTab();
+});
 
-    delayedClicks(button, 4).then(function() {
-      checkResults("_PAINTFLASHING_");
-    });
-  }).then(null, console.error);
+function* testButton(toolbox, Telemetry) {
+  info("Testing command-button-paintflashing");
+
+  let button = toolbox.doc.querySelector("#command-button-paintflashing");
+  ok(button, "Captain, we have the button");
+
+  yield delayedClicks(button, 4);
+  checkResults("_PAINTFLASHING_", Telemetry);
 }
 
 function delayedClicks(node, clicks) {
-  let deferred = promise.defer();
-  let clicked = 0;
-
-  // See TOOL_DELAY for why we need setTimeout here
-  setTimeout(function delayedClick() {
-    info("Clicking button " + node.id);
-    node.click();
-    clicked++;
+  return new Promise(resolve => {
+    let clicked = 0;
 
-    if (clicked >= clicks) {
-      deferred.resolve(node);
-    } else {
-      setTimeout(delayedClick, TOOL_DELAY);
-    }
-  }, TOOL_DELAY);
+    // See TOOL_DELAY for why we need setTimeout here
+    setTimeout(function delayedClick() {
+      info("Clicking button " + node.id);
+      node.click();
+      clicked++;
 
-  return deferred.promise;
+      if (clicked >= clicks) {
+        resolve(node);
+      } else {
+        setTimeout(delayedClick, TOOL_DELAY);
+      }
+    }, TOOL_DELAY);
+  });
 }
 
-function checkResults(histIdFocus) {
+function checkResults(histIdFocus, Telemetry) {
   let result = Telemetry.prototype.telemetryInfo;
 
   for (let [histId, value] of Iterator(result)) {
     if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
         !histId.contains(histIdFocus)) {
       // Inspector stats are tested in
       // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
       // because we only open the inspector once for this test.
@@ -98,34 +81,9 @@ function checkResults(histIdFocus) {
 
       let okay = value.every(function(element) {
         return element > 0;
       });
 
       ok(okay, "All " + histId + " entries have time > 0");
     }
   }
-
-  finishUp();
 }
-
-function finishUp() {
-  gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
--- a/browser/devtools/shared/test/browser_telemetry_button_responsive.js
+++ b/browser/devtools/shared/test/browser_telemetry_button_responsive.js
@@ -1,81 +1,64 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_button_responsive.js</p>";
+const TEST_URI = "data:text/html;charset=utf-8," +
+  "<p>browser_telemetry_button_responsive.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  };
-
-  testButton("command-button-responsive");
-}
-
-function testButton(id) {
-  info("Testing " + id);
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
   let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  info("inspector opened");
 
-  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
-    info("inspector opened");
+  info("testing the responsivedesign button");
+  yield testButton(toolbox, Telemetry);
 
-    let button = toolbox.doc.querySelector("#" + id);
-    ok(button, "Captain, we have the button");
+  stopRecordingTelemetryLogs(Telemetry);
+  yield gDevTools.closeToolbox(target);
+  gBrowser.removeCurrentTab();
+});
 
-    delayedClicks(button, 4).then(function() {
-      checkResults("_RESPONSIVE_");
-    });
-  }).then(null, console.error);
+function* testButton(toolbox, Telemetry) {
+  info("Testing command-button-responsive");
+
+  let button = toolbox.doc.querySelector("#command-button-responsive");
+  ok(button, "Captain, we have the button");
+
+  yield delayedClicks(button, 4);
+  checkResults("_RESPONSIVE_", Telemetry);
 }
 
 function delayedClicks(node, clicks) {
-  let deferred = promise.defer();
-  let clicked = 0;
-
-  // See TOOL_DELAY for why we need setTimeout here
-  setTimeout(function delayedClick() {
-    info("Clicking button " + node.id);
-    node.click();
-    clicked++;
+  return new Promise(resolve => {
+    let clicked = 0;
 
-    if (clicked >= clicks) {
-      deferred.resolve(node);
-    } else {
-      setTimeout(delayedClick, TOOL_DELAY);
-    }
-  }, TOOL_DELAY);
+    // See TOOL_DELAY for why we need setTimeout here
+    setTimeout(function delayedClick() {
+      info("Clicking button " + node.id);
+      node.click();
+      clicked++;
 
-  return deferred.promise;
+      if (clicked >= clicks) {
+        resolve(node);
+      } else {
+        setTimeout(delayedClick, TOOL_DELAY);
+      }
+    }, TOOL_DELAY);
+  });
 }
 
-function checkResults(histIdFocus) {
+function checkResults(histIdFocus, Telemetry) {
   let result = Telemetry.prototype.telemetryInfo;
 
   for (let [histId, value] of Iterator(result)) {
     if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
         !histId.contains(histIdFocus)) {
       // Inspector stats are tested in
       // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
       // because we only open the inspector once for this test.
@@ -98,34 +81,9 @@ function checkResults(histIdFocus) {
 
       let okay = value.every(function(element) {
         return element > 0;
       });
 
       ok(okay, "All " + histId + " entries have time > 0");
     }
   }
-
-  finishUp();
 }
-
-function finishUp() {
-  gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
--- a/browser/devtools/shared/test/browser_telemetry_button_scratchpad.js
+++ b/browser/devtools/shared/test/browser_telemetry_button_scratchpad.js
@@ -1,116 +1,102 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-///////////////////
-//
-// Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed.
-//
-thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Protocol error (unknownError): Error: Got an invalid root window in DocumentWalker");
-
-const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_button_scratchpad.js</p>";
+const TEST_URI = "data:text/html;charset=utf-8," +
+  "<p>browser_telemetry_button_scratchpad.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-let numScratchpads = 0;
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  };
-
-  Services.ww.registerNotification(windowObserver);
-  testButton("command-button-scratchpad");
-}
-
-function testButton(id) {
-  info("Testing " + id);
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
   let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  info("inspector opened");
 
-  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
-    info("inspector opened");
+  let onAllWindowsOpened = trackScratchpadWindows();
+
+  info("testing the scratchpad button");
+  yield testButton(toolbox, Telemetry);
+  yield onAllWindowsOpened;
+
+  checkResults("_SCRATCHPAD_", Telemetry);
+
+  stopRecordingTelemetryLogs(Telemetry);
+  yield gDevTools.closeToolbox(target);
+  gBrowser.removeCurrentTab();
+});
+
+function trackScratchpadWindows() {
+  info("register the window observer to track when scratchpad windows open");
+
+  let numScratchpads = 0;
 
-    let button = toolbox.doc.querySelector("#" + id);
-    ok(button, "Captain, we have the button");
+  return new Promise(resolve => {
+    Services.ww.registerNotification(function observer(subject, topic) {
+      if (topic == "domwindowopened") {
+        let win = subject.QueryInterface(Ci.nsIDOMWindow);
+        win.addEventListener("load", function onLoad() {
+          win.removeEventListener("load", onLoad, false);
+
+          if (win.Scratchpad) {
+            win.Scratchpad.addObserver({
+              onReady: function() {
+                win.Scratchpad.removeObserver(this);
+                numScratchpads++;
+                win.close();
 
-    delayedClicks(button, 4).then(null, console.error);
-  }).then(null, console.error);
+                info("another scratchpad was opened and closed, count is now " + numScratchpads);
+
+                if (numScratchpads === 4) {
+                  Services.ww.unregisterNotification(observer);
+                  info("4 scratchpads have been opened and closed, checking results");
+                  resolve();
+                }
+              },
+            });
+          }
+        }, false);
+      }
+    });
+  });
 }
 
-function windowObserver(aSubject, aTopic, aData) {
-  if (aTopic == "domwindowopened") {
-    let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
-    win.addEventListener("load", function onLoad() {
-      win.removeEventListener("load", onLoad, false);
-
-      if (win.Scratchpad) {
-        win.Scratchpad.addObserver({
-          onReady: function() {
-            win.Scratchpad.removeObserver(this);
-            numScratchpads++;
-            win.close();
+function* testButton(toolbox, Telemetry) {
+  info("Testing command-button-scratchpad");
+  let button = toolbox.doc.querySelector("#command-button-scratchpad");
+  ok(button, "Captain, we have the button");
 
-            info("another scratchpad was opened and closed, count is now " + numScratchpads);
-
-            if (numScratchpads === 4) {
-              Services.ww.unregisterNotification(windowObserver);
-              info("4 scratchpads have been opened and closed, checking results");
-              checkResults("_SCRATCHPAD_");
-            }
-          },
-        });
-      }
-    }, false);
-  }
+  yield delayedClicks(button, 4);
 }
 
 function delayedClicks(node, clicks) {
-  let deferred = promise.defer();
-  let clicked = 0;
-
-  // See TOOL_DELAY for why we need setTimeout here
-  setTimeout(function delayedClick() {
-    info("Clicking button " + node.id);
-    node.click();
-    clicked++;
+  return new Promise(resolve => {
+    let clicked = 0;
 
-    if (clicked >= clicks) {
-      deferred.resolve(node);
-    } else {
-      setTimeout(delayedClick, TOOL_DELAY);
-    }
-  }, TOOL_DELAY);
+    // See TOOL_DELAY for why we need setTimeout here
+    setTimeout(function delayedClick() {
+      info("Clicking button " + node.id);
+      node.click();
+      clicked++;
 
-  return deferred.promise;
+      if (clicked >= clicks) {
+        resolve(node);
+      } else {
+        setTimeout(delayedClick, TOOL_DELAY);
+      }
+    }, TOOL_DELAY);
+  });
 }
 
-function checkResults(histIdFocus) {
+function checkResults(histIdFocus, Telemetry) {
   let result = Telemetry.prototype.telemetryInfo;
 
   for (let [histId, value] of Iterator(result)) {
     if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
         !histId.contains(histIdFocus)) {
       // Inspector stats are tested in
       // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
       // because we only open the inspector once for this test.
@@ -133,34 +119,9 @@ function checkResults(histIdFocus) {
 
       let okay = value.every(function(element) {
         return element > 0;
       });
 
       ok(okay, "All " + histId + " entries have time > 0");
     }
   }
-
-  finishUp();
 }
-
-function finishUp() {
-  gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = numScratchpads = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
--- a/browser/devtools/shared/test/browser_telemetry_button_tilt.js
+++ b/browser/devtools/shared/test/browser_telemetry_button_tilt.js
@@ -1,81 +1,64 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_button_tilt.js</p>";
+const TEST_URI = "data:text/html;charset=utf-8," +
+  "<p>browser_telemetry_button_tilt.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  };
-
-  testButton("command-button-tilt");
-}
-
-function testButton(id) {
-  info("Testing " + id);
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
   let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  info("inspector opened");
 
-  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
-    info("inspector opened");
+  info("testing the tilt button");
+  yield testButton(toolbox, Telemetry);
 
-    let button = toolbox.doc.querySelector("#" + id);
-    ok(button, "Captain, we have the button");
+  stopRecordingTelemetryLogs(Telemetry);
+  yield gDevTools.closeToolbox(target);
+  gBrowser.removeCurrentTab();
+});
 
-    delayedClicks(button, 4).then(function() {
-      checkResults("_TILT_");
-    });
-  }).then(null, console.error);
+function* testButton(toolbox, Telemetry) {
+  info("Testing command-button-tilt");
+
+  let button = toolbox.doc.querySelector("#command-button-tilt");
+  ok(button, "Captain, we have the button");
+
+  yield delayedClicks(button, 4)
+  checkResults("_TILT_", Telemetry);
 }
 
 function delayedClicks(node, clicks) {
-  let deferred = promise.defer();
-  let clicked = 0;
-
-  // See TOOL_DELAY for why we need setTimeout here
-  setTimeout(function delayedClick() {
-    info("Clicking button " + node.id);
-    node.click();
-    clicked++;
+  return new Promise(resolve => {
+    let clicked = 0;
 
-    if (clicked >= clicks) {
-      deferred.resolve(node);
-    } else {
-      setTimeout(delayedClick, TOOL_DELAY);
-    }
-  }, TOOL_DELAY);
+    // See TOOL_DELAY for why we need setTimeout here
+    setTimeout(function delayedClick() {
+      info("Clicking button " + node.id);
+      node.click();
+      clicked++;
 
-  return deferred.promise;
+      if (clicked >= clicks) {
+        resolve(node);
+      } else {
+        setTimeout(delayedClick, TOOL_DELAY);
+      }
+    }, TOOL_DELAY);
+  });
 }
 
-function checkResults(histIdFocus) {
+function checkResults(histIdFocus, Telemetry) {
   let result = Telemetry.prototype.telemetryInfo;
 
   for (let [histId, value] of Iterator(result)) {
     if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
         !histId.contains(histIdFocus)) {
       // Inspector stats are tested in
       // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
       // because we only open the inspector once for this test.
@@ -98,34 +81,9 @@ function checkResults(histIdFocus) {
 
       let okay = value.every(function(element) {
         return element > 0;
       });
 
       ok(okay, "All " + histId + " entries have time > 0");
     }
   }
-
-  finishUp();
 }
-
-function finishUp() {
-  gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
--- a/browser/devtools/shared/test/browser_telemetry_sidebar.js
+++ b/browser/devtools/shared/test/browser_telemetry_sidebar.js
@@ -2,71 +2,59 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_sidebar.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, "inspector");
+  info("inspector opened");
 
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
+  yield testSidebar(toolbox);
+  checkResults(Telemetry);
 
-      this.telemetryInfo[histogramId].push(value);
-    }
-  };
+  stopRecordingTelemetryLogs(Telemetry);
+  yield gDevTools.closeToolbox(target);
+  gBrowser.removeCurrentTab();
+});
 
-  testSidebar();
-}
-
-function testSidebar() {
+function* testSidebar(toolbox) {
   info("Testing sidebar");
 
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let inspector = toolbox.getCurrentPanel();
+  let sidebarTools = ["ruleview", "computedview", "fontinspector",
+                      "layoutview", "animationinspector"];
 
-  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
-    let inspector = toolbox.getCurrentPanel();
-    let sidebarTools = ["ruleview", "computedview", "fontinspector",
-                        "layoutview", "animationinspector"];
+  // Concatenate the array with itself so that we can open each tool twice.
+  sidebarTools.push.apply(sidebarTools, sidebarTools);
 
-    // Concatenate the array with itself so that we can open each tool twice.
-    sidebarTools.push.apply(sidebarTools, sidebarTools);
-
+  return new Promise(resolve => {
     // See TOOL_DELAY for why we need setTimeout here
     setTimeout(function selectSidebarTab() {
       let tool = sidebarTools.pop();
       if (tool) {
         inspector.sidebar.select(tool);
         setTimeout(function() {
           setTimeout(selectSidebarTab, TOOL_DELAY);
         }, TOOL_DELAY);
       } else {
-        checkResults();
+        resolve();
       }
     }, TOOL_DELAY);
   });
 }
 
-function checkResults() {
+function checkResults(Telemetry) {
   let result = Telemetry.prototype.telemetryInfo;
 
   for (let [histId, value] of Iterator(result)) {
     if (histId.startsWith("DEVTOOLS_INSPECTOR_")) {
       // Inspector stats are tested in browser_telemetry_toolboxtabs.js so we
       // skip them here because we only open the inspector once for this test.
       continue;
     }
@@ -89,34 +77,9 @@ function checkResults() {
 
       let okay = value.every(function(element) {
         return element > 0;
       });
 
       ok(okay, "All " + histId + " entries have time > 0");
     }
   }
-
-  finishUp();
 }
-
-function finishUp() {
-  gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
--- a/browser/devtools/shared/test/browser_telemetry_toolbox.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolbox.js
@@ -1,103 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolbox.js</p>";
+const TEST_URI = "data:text/html;charset=utf-8," +
+  "<p>browser_telemetry_toolbox.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  };
-
-  openToolboxThreeTimes();
-}
-
-let pass = 0;
-function openToolboxThreeTimes() {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
-    info("Toolbox opened");
-
-    toolbox.once("destroyed", function() {
-      if (pass++ === 3) {
-        checkResults();
-      } else {
-        openToolboxThreeTimes();
-      }
-    });
-    // We use a timeout to check the toolbox's active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, console.error);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
-
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
+  yield openAndCloseToolbox(3, TOOL_DELAY, "inspector");
+  checkTelemetryResults(Telemetry);
 
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
+});
--- a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js
@@ -1,117 +1,27 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_canvasdebugger.js</p>";
+const TEST_URI = "data:text/html;charset=utf-8," +
+  "<p>browser_telemetry_toolboxtabs_canvasdebugger.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-let originalPref = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
-Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true);
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  }
-
-  openToolboxTabTwice("canvasdebugger", false);
-}
-
-function openToolboxTabTwice(id, secondPass) {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, id).then(function(toolbox) {
-    info("Toolbox tab " + id + " opened");
+add_task(function*() {
+  info("Activate the canvasdebugger");
+  let originalPref = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
+  Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true);
 
-    toolbox.once("destroyed", function() {
-      if (secondPass) {
-        checkResults();
-      } else {
-        openToolboxTabTwice(id, true);
-      }
-    });
-    // We use a timeout to check the tools active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, reportError);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
-
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
+  yield openAndCloseToolbox(2, TOOL_DELAY, "canvasdebugger");
+  checkTelemetryResults(Telemetry);
 
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
-
-  finishUp();
-}
-
-function reportError(error) {
-  let stack = "    " + error.stack.replace(/\n?.*?@/g, "\n    JS frame :: ");
-
-  ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
-            error.lineNumber + "\n\nStack trace:" + stack);
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
 
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
+  info("De-activate the canvasdebugger");
   Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", originalPref);
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
+});
--- a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js
@@ -1,114 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_inspector.js</p>";
+const TEST_URI = "data:text/html;charset=utf-8," +
+  "<p>browser_telemetry_toolboxtabs_inspector.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  }
-
-  openToolboxTabTwice("inspector", false);
-}
-
-function openToolboxTabTwice(id, secondPass) {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, id).then(function(toolbox) {
-    info("Toolbox tab " + id + " opened");
-
-    toolbox.once("destroyed", function() {
-      if (secondPass) {
-        checkResults();
-      } else {
-        openToolboxTabTwice(id, true);
-      }
-    });
-    // We use a timeout to check the tools active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, reportError);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
-
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
-
-  finishUp();
-}
+  yield openAndCloseToolbox(2, TOOL_DELAY, "inspector");
+  checkTelemetryResults(Telemetry);
 
-function reportError(error) {
-  let stack = "    " + error.stack.replace(/\n?.*?@/g, "\n    JS frame :: ");
-
-  ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
-            error.lineNumber + "\n\nStack trace:" + stack);
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
+});
--- a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
@@ -1,114 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_jsdebugger.js</p>";
+const TEST_URI = "data:text/html;charset=utf-8," +
+  "<p>browser_telemetry_toolboxtabs_jsdebugger.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  }
-
-  openToolboxTabTwice("jsdebugger", false);
-}
-
-function openToolboxTabTwice(id, secondPass) {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, id).then(function(toolbox) {
-    info("Toolbox tab " + id + " opened");
-
-    toolbox.once("destroyed", function() {
-      if (secondPass) {
-        checkResults();
-      } else {
-        openToolboxTabTwice(id, true);
-      }
-    });
-    // We use a timeout to check the tools active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, reportError);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
-
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
-
-  finishUp();
-}
+  yield openAndCloseToolbox(2, TOOL_DELAY, "jsdebugger");
+  checkTelemetryResults(Telemetry);
 
-function reportError(error) {
-  let stack = "    " + error.stack.replace(/\n?.*?@/g, "\n    JS frame :: ");
-
-  ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
-            error.lineNumber + "\n\nStack trace:" + stack);
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
+});
--- a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
@@ -2,113 +2,18 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_jsprofiler.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  }
-
-  openToolboxTabTwice("jsprofiler", false);
-}
-
-function openToolboxTabTwice(id, secondPass) {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, id).then(function(toolbox) {
-    info("Toolbox tab " + id + " opened");
-
-    toolbox.once("destroyed", function() {
-      if (secondPass) {
-        checkResults();
-      } else {
-        openToolboxTabTwice(id, true);
-      }
-    });
-    // We use a timeout to check the tools active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, reportError);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
-
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
-
-  finishUp();
-}
+  yield openAndCloseToolbox(2, TOOL_DELAY, "jsprofiler");
+  checkTelemetryResults(Telemetry);
 
-function reportError(error) {
-  let stack = "    " + error.stack.replace(/\n?.*?@/g, "\n    JS frame :: ");
-
-  ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
-            error.lineNumber + "\n\nStack trace:" + stack);
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
+});
--- a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
@@ -2,113 +2,19 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_netmonitor.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  }
-
-  openToolboxTabTwice("netmonitor", false);
-}
-
-function openToolboxTabTwice(id, secondPass) {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, id).then(function(toolbox) {
-    info("Toolbox tab " + id + " opened");
-
-    toolbox.once("destroyed", function() {
-      if (secondPass) {
-        checkResults();
-      } else {
-        openToolboxTabTwice(id, true);
-      }
-    });
-    // We use a timeout to check the tools active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, reportError);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
-
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
-
-  finishUp();
-}
+  yield openAndCloseToolbox(2, TOOL_DELAY, "netmonitor");
+  checkTelemetryResults(Telemetry);
 
-function reportError(error) {
-  let stack = "    " + error.stack.replace(/\n?.*?@/g, "\n    JS frame :: ");
-
-  ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
-            error.lineNumber + "\n\nStack trace:" + stack);
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
+});
 
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
--- a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_options.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_options.js
@@ -2,113 +2,18 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_options.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  }
-
-  openToolboxTabTwice("options", false);
-}
-
-function openToolboxTabTwice(id, secondPass) {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, id).then(function(toolbox) {
-    info("Toolbox tab " + id + " opened");
-
-    toolbox.once("destroyed", function() {
-      if (secondPass) {
-        checkResults();
-      } else {
-        openToolboxTabTwice(id, true);
-      }
-    });
-    // We use a timeout to check the tools active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, reportError);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
-
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
-
-  finishUp();
-}
+  yield openAndCloseToolbox(2, TOOL_DELAY, "options");
+  checkTelemetryResults(Telemetry);
 
-function reportError(error) {
-  let stack = "    " + error.stack.replace(/\n?.*?@/g, "\n    JS frame :: ");
-
-  ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
-            error.lineNumber + "\n\nStack trace:" + stack);
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
+});
--- a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_shadereditor.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_shadereditor.js
@@ -1,124 +1,33 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 ///////////////////
 //
 // Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed. 
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
 //
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_shadereditor.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-let originalPref = Services.prefs.getBoolPref("devtools.shadereditor.enabled");
-Services.prefs.setBoolPref("devtools.shadereditor.enabled", true);
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  }
-
-  openToolboxTabTwice("shadereditor", false);
-}
-
-function openToolboxTabTwice(id, secondPass) {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, id).then(function(toolbox) {
-    info("Toolbox tab " + id + " opened");
+add_task(function*() {
+  info("Active the sharer editor");
+  let originalPref = Services.prefs.getBoolPref("devtools.shadereditor.enabled");
+  Services.prefs.setBoolPref("devtools.shadereditor.enabled", true);
 
-    toolbox.once("destroyed", function() {
-      if (secondPass) {
-        checkResults();
-      } else {
-        openToolboxTabTwice(id, true);
-      }
-    });
-    // We use a timeout to check the tools active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, reportError);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
-
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
+  yield openAndCloseToolbox(2, TOOL_DELAY, "shadereditor");
+  checkTelemetryResults(Telemetry);
 
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
-
-  finishUp();
-}
-
-function reportError(error) {
-  let stack = "    " + error.stack.replace(/\n?.*?@/g, "\n    JS frame :: ");
-
-  ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
-            error.lineNumber + "\n\nStack trace:" + stack);
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
 
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
+  info("De-activate the sharer editor");
   Services.prefs.setBoolPref("devtools.shadereditor.enabled", originalPref);
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
+});
--- a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_storage.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_storage.js
@@ -2,118 +2,24 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_storage.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-let STORAGE_PREF = "devtools.storage.enabled";
-Services.prefs.setBoolPref(STORAGE_PREF, true);
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  }
-
-  openToolboxTabTwice("storage", false);
-}
-
-function openToolboxTabTwice(id, secondPass) {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, id).then(function(toolbox) {
-    info("Toolbox tab " + id + " opened");
+add_task(function*() {
+  info("Activating the storage inspector");
+  Services.prefs.setBoolPref("devtools.storage.enabled", true);
 
-    toolbox.once("destroyed", function() {
-      if (secondPass) {
-        checkResults();
-      } else {
-        openToolboxTabTwice(id, true);
-      }
-    });
-    // We use a timeout to check the tools active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, reportError);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
-
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
+  yield openAndCloseToolbox(2, TOOL_DELAY, "storage");
+  checkTelemetryResults(Telemetry);
 
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
-
-  finishUp();
-}
-
-function reportError(error) {
-  let stack = "    " + error.stack.replace(/\n?.*?@/g, "\n    JS frame :: ");
-
-  ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
-            error.lineNumber + "\n\nStack trace:" + stack);
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
 
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  Services.prefs.clearUserPref(STORAGE_PREF);
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
+  info("De-activating the storage inspector");
+  Services.prefs.clearUserPref("devtools.storage.enabled");
+});
--- a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
@@ -2,113 +2,19 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_styleeditor.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  }
-
-  openToolboxTabTwice("styleeditor", false);
-}
-
-function openToolboxTabTwice(id, secondPass) {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, id).then(function(toolbox) {
-    info("Toolbox tab " + id + " opened");
-
-    toolbox.once("destroyed", function() {
-      if (secondPass) {
-        checkResults();
-      } else {
-        openToolboxTabTwice(id, true);
-      }
-    });
-    // We use a timeout to check the tools active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, reportError);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
-
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
-
-  finishUp();
-}
+  yield openAndCloseToolbox(2, TOOL_DELAY, "styleeditor");
+  checkTelemetryResults(Telemetry);
 
-function reportError(error) {
-  let stack = "    " + error.stack.replace(/\n?.*?@/g, "\n    JS frame :: ");
-
-  ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
-            error.lineNumber + "\n\nStack trace:" + stack);
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
+});
 
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
--- a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js
@@ -2,117 +2,25 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_webaudioeditor.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-let originalPref = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled");
-Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true);
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  }
-
-  openToolboxTabTwice("webaudioeditor", false);
-}
-
-function openToolboxTabTwice(id, secondPass) {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, id).then(function(toolbox) {
-    info("Toolbox tab " + id + " opened");
+add_task(function*() {
+  info("Activating the webaudioeditor");
+  let originalPref = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled");
+  Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true);
 
-    toolbox.once("destroyed", function() {
-      if (secondPass) {
-        checkResults();
-      } else {
-        openToolboxTabTwice(id, true);
-      }
-    });
-    // We use a timeout to check the tools active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, reportError);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
-
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
+  yield openAndCloseToolbox(2, TOOL_DELAY, "webaudioeditor");
+  checkTelemetryResults(Telemetry);
 
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
-
-  finishUp();
-}
-
-function reportError(error) {
-  let stack = "    " + error.stack.replace(/\n?.*?@/g, "\n    JS frame :: ");
-
-  ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
-            error.lineNumber + "\n\nStack trace:" + stack);
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
 
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
+  info("De-activating the webaudioeditor");
   Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", originalPref);
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
+});
--- a/browser/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js
+++ b/browser/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js
@@ -2,113 +2,18 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_styleeditor_webconsole.js</p>";
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
-let {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
-let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-let Telemetry = require("devtools/shared/telemetry");
-
-function init() {
-  Telemetry.prototype.telemetryInfo = {};
-  Telemetry.prototype._oldlog = Telemetry.prototype.log;
-  Telemetry.prototype.log = function(histogramId, value) {
-    if (!this.telemetryInfo) {
-      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
-      return;
-    }
-    if (histogramId) {
-      if (!this.telemetryInfo[histogramId]) {
-        this.telemetryInfo[histogramId] = [];
-      }
-
-      this.telemetryInfo[histogramId].push(value);
-    }
-  }
-
-  openToolboxTabTwice("webconsole", false);
-}
-
-function openToolboxTabTwice(id, secondPass) {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gDevTools.showToolbox(target, id).then(function(toolbox) {
-    info("Toolbox tab " + id + " opened");
-
-    toolbox.once("destroyed", function() {
-      if (secondPass) {
-        checkResults();
-      } else {
-        openToolboxTabTwice(id, true);
-      }
-    });
-    // We use a timeout to check the tools active time
-    setTimeout(function() {
-      gDevTools.closeToolbox(target);
-    }, TOOL_DELAY);
-  }).then(null, reportError);
-}
-
-function checkResults() {
-  let result = Telemetry.prototype.telemetryInfo;
+add_task(function*() {
+  yield promiseTab(TEST_URI);
+  let Telemetry = loadTelemetryAndRecordLogs();
 
-  for (let [histId, value] of Iterator(result)) {
-    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
-      ok(value.length === 1 && value[0] === true,
-         "Per user value " + histId + " has a single value of true");
-    } else if (histId.endsWith("OPENED_BOOLEAN")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element === true;
-      });
-
-      ok(okay, "All " + histId + " entries are === true");
-    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
-      ok(value.length > 1, histId + " has more than one entry");
-
-      let okay = value.every(function(element) {
-        return element > 0;
-      });
-
-      ok(okay, "All " + histId + " entries have time > 0");
-    }
-  }
-
-  finishUp();
-}
+  yield openAndCloseToolbox(2, TOOL_DELAY, "webconsole");
+  checkTelemetryResults(Telemetry);
 
-function reportError(error) {
-  let stack = "    " + error.stack.replace(/\n?.*?@/g, "\n    JS frame :: ");
-
-  ok(false, "ERROR: " + error + " at " + error.fileName + ":" +
-            error.lineNumber + "\n\nStack trace:" + stack);
-  finishUp();
-}
-
-function finishUp() {
+  stopRecordingTelemetryLogs(Telemetry);
   gBrowser.removeCurrentTab();
-
-  Telemetry.prototype.log = Telemetry.prototype._oldlog;
-  delete Telemetry.prototype._oldlog;
-  delete Telemetry.prototype.telemetryInfo;
-
-  TargetFactory = Services = promise = require = null;
-
-  finish();
-}
-
-function test() {
-  waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(init, content);
-  }, true);
-
-  content.location = TEST_URI;
-}
+});
--- a/browser/devtools/shared/test/browser_templater_basic.js
+++ b/browser/devtools/shared/test/browser_templater_basic.js
@@ -4,33 +4,33 @@
 // Tests that the DOM Template engine works properly
 
 /*
  * These tests run both in Mozilla/Mochitest and plain browsers (as does
  * domtemplate)
  * We should endevour to keep the source in sync.
  */
 
-var promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
-var template = Cu.import("resource://gre/modules/devtools/Templater.jsm", {}).template;
+const template = Cu.import("resource://gre/modules/devtools/Templater.jsm", {}).template;
 
 const TEST_URI = TEST_URI_ROOT + "browser_templater_basic.html";
 
-function test() {
-  addTab(TEST_URI, function() {
-    info("Starting DOM Templater Tests");
-    runTest(0);
-  });
-}
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
-function runTest(index) {
+  info("Starting DOM Templater Tests");
+  runTest(0, host, doc);
+});
+
+function runTest(index, host, doc) {
   var options = tests[index] = tests[index]();
-  var holder = content.document.createElement('div');
+  var holder = doc.createElement('div');
   holder.id = options.name;
-  var body = content.document.body;
+  var body = doc.body;
   body.appendChild(holder);
   holder.innerHTML = options.template;
 
   info('Running ' + options.name);
   template(holder, options.data, options.options);
 
   if (typeof options.result == 'string') {
     is(holder.innerHTML, options.result, options.name);
@@ -42,20 +42,20 @@ function runTest(index) {
 
   if (options.also) {
     options.also(options);
   }
 
   function runNextTest() {
     index++;
     if (index < tests.length) {
-      runTest(index);
+      runTest(index, host, doc);
     }
     else {
-      finished();
+      finished(host);
     }
   }
 
   if (options.later) {
     var ais = is.bind(this);
 
     function createTester(holder, options) {
       return () => {
@@ -66,17 +66,18 @@ function runTest(index) {
 
     executeSoon(createTester(holder, options));
   }
   else {
     runNextTest();
   }
 }
 
-function finished() {
+function finished(host) {
+  host.destroy();
   gBrowser.removeCurrentTab();
   info("Finishing DOM Templater Tests");
   tests = null;
   finish();
 }
 
 /**
  * Why have an array of functions that return data rather than just an array
@@ -275,14 +276,10 @@ var tests = [
     template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
     data: { nullvar: null, undefinedvar1: undefined },
     options: { blankNullUndefined: true },
     result: '<div><p value=""></p><p value=""></p><p value=""></p></div>'
   };}
 ];
 
 function delayReply(data) {
-  var d = promise.defer();
-  executeSoon(function() {
-    d.resolve(data);
-  });
-  return d.promise;
+  return new Promise(resolve => resolve(data));
 }
--- a/browser/devtools/shared/test/browser_theme.js
+++ b/browser/devtools/shared/test/browser_theme.js
@@ -1,31 +1,21 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that theme utilities work
 
-let { Cu } = devtools.require("chrome");
-let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
-let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
-let { getColor, getTheme, setTheme } = devtools.require("devtools/shared/theme");
+let {getColor, getTheme, setTheme} = devtools.require("devtools/shared/theme");
 
 function test() {
-  waitForExplicitFinish();
   testGetTheme();
   testSetTheme();
   testGetColor();
   testColorExistence();
-  endTests();
-}
-
-function endTests() {
-  gDevTools = Services = null;
-  finish();
 }
 
 function testGetTheme () {
   let originalTheme = getTheme();
   ok(originalTheme, "has some theme to start with.");
   Services.prefs.setCharPref("devtools.theme", "light");
   is(getTheme(), "light", "getTheme() correctly returns light theme");
   Services.prefs.setCharPref("devtools.theme", "dark");
--- a/browser/devtools/shared/test/browser_treeWidget_basic.js
+++ b/browser/devtools/shared/test/browser_treeWidget_basic.js
@@ -5,46 +5,36 @@
 // Tests that the tree widget api works fine
 
 const TEST_URI = "data:text/html;charset=utf-8,<head><link rel='stylesheet' " +
   "type='text/css' href='chrome://browser/skin/devtools/common.css'><link " +
   "rel='stylesheet' type='text/css' href='chrome://browser/skin/devtools/widg" +
   "ets.css'></head><body><div></div><span></span></body>";
 const {TreeWidget} = devtools.require("devtools/shared/widgets/TreeWidget");
 
-let doc, tree;
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
-function test() {
-  waitForExplicitFinish();
-  addTab(TEST_URI, () => {
-    doc = content.document;
-    tree = new TreeWidget(doc.querySelector("div"), {
-      defaultType: "store"
-    });
-    startTests();
+  let tree = new TreeWidget(doc.querySelector("div"), {
+    defaultType: "store"
   });
-}
 
-function endTests() {
+  populateTree(tree, doc);
+  testTreeItemInsertedCorrectly(tree, doc);
+  testAPI(tree, doc);
+  populateUnsortedTree(tree, doc);
+  testUnsortedTreeItemInsertedCorrectly(tree, doc);
+
   tree.destroy();
-  doc = tree = null;
+  host.destroy();
   gBrowser.removeCurrentTab();
-  finish();
-}
+});
 
-function startTests() {
-  populateTree();
-  testTreeItemInsertedCorrectly();
-  testAPI();
-  populateUnsortedTree();
-  testUnsortedTreeItemInsertedCorrectly();
-  endTests();
-}
-
-function populateTree() {
+function populateTree(tree, doc) {
   tree.add([{
     id: "level1",
     label: "Level 1"
   }, {
     id: "level2-1",
     label: "Level 2"
   }, {
     id: "level3-1",
@@ -72,17 +62,17 @@ function populateTree() {
     type: "js"
   }]);
   tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
 }
 
 /**
  * Test if the nodes are inserted correctly in the tree.
  */
-function testTreeItemInsertedCorrectly() {
+function testTreeItemInsertedCorrectly(tree, doc) {
   is(tree.root.children.children.length, 2, "Number of top level elements match");
   is(tree.root.children.firstChild.lastChild.children.length, 3,
      "Number of first second level elements match");
   is(tree.root.children.lastChild.lastChild.children.length, 1,
      "Number of second second level elements match");
 
   ok(tree.root.items.has("level1"), "Level1 top level element exists");
   is(tree.root.children.firstChild.dataset.id, JSON.stringify(["level1"]),
@@ -119,29 +109,29 @@ function testTreeItemInsertedCorrectly()
      "Data id of last top level element matches");
   is(tree.root.children.firstChild.firstChild.firstChild, node,
      "Newly added node is inserted at the right location");
 }
 
 /**
  * Populate the unsorted tree.
  */
-function populateUnsortedTree() {
+function populateUnsortedTree(tree, doc) {
   tree.sorted = false;
 
   tree.add([{ id: "g-1", label: "g-1"}])
   tree.add(["g-1", { id: "d-2", label: "d-2.1"}]);
   tree.add(["g-1", { id: "b-2", label: "b-2.2"}]);
   tree.add(["g-1", { id: "a-2", label: "a-2.3"}]);
 }
 
 /**
  * Test if the nodes are inserted correctly in the unsorted tree.
  */
-function testUnsortedTreeItemInsertedCorrectly() {
+function testUnsortedTreeItemInsertedCorrectly(tree, doc) {
   ok(tree.root.items.has("g-1"), "g-1 top level element exists");
 
   is(tree.root.children.firstChild.lastChild.children.length, 3,
     "Number of children for g-1 matches");
   is(tree.root.children.firstChild.dataset.id, JSON.stringify(["g-1"]),
     "Data id of g-1 matches");
   is(tree.root.children.firstChild.firstChild.textContent, "g-1",
     "Text content of g-1 matches");
@@ -154,17 +144,17 @@ function testUnsortedTreeItemInsertedCor
     "b-2.2", "Text content of b-2 matches");
   is(tree.root.children.firstChild.lastChild.lastChild.textContent, "a-2.3",
     "Text content of a-2 matches");
 }
 
 /**
  * Tests if the API exposed by TreeWidget works properly
  */
-function testAPI() {
+function testAPI(tree, doc) {
   info("Testing TreeWidget API");
   // Check if selectItem and selectedItem setter works as expected
   // Nothing should be selected beforehand
   ok(!doc.querySelector(".theme-selected"), "Nothing is selected");
   tree.selectItem(["level1"]);
   let node = doc.querySelector(".theme-selected");
   ok(!!node, "Something got selected");
   is(node.parentNode.dataset.id, JSON.stringify(["level1"]),
--- a/browser/devtools/shared/test/browser_treeWidget_keyboard_interaction.js
+++ b/browser/devtools/shared/test/browser_treeWidget_keyboard_interaction.js
@@ -4,46 +4,35 @@
 
 // Tests that keyboard interaction works fine with the tree widget
 
 const TEST_URI = "data:text/html;charset=utf-8,<head><link rel='stylesheet' " +
   "type='text/css' href='chrome://browser/skin/devtools/common.css'><link " +
   "rel='stylesheet' type='text/css' href='chrome://browser/skin/devtools/widg" +
   "ets.css'></head><body><div></div><span></span></body>";
 const {TreeWidget} = devtools.require("devtools/shared/widgets/TreeWidget");
-let {Task} = devtools.require("resource://gre/modules/Task.jsm");
-let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-
-let doc, tree;
+const {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
-function test() {
-  waitForExplicitFinish();
-  addTab(TEST_URI, () => {
-    doc = content.document;
-    tree = new TreeWidget(doc.querySelector("div"), {
-      defaultType: "store"
-    });
-    startTests();
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+  let tree = new TreeWidget(doc.querySelector("div"), {
+    defaultType: "store"
   });
-}
 
-function endTests() {
+  populateTree(tree, doc);
+  yield testKeyboardInteraction(tree, win);
+
   tree.destroy();
-  doc = tree = null;
+  host.destroy();
   gBrowser.removeCurrentTab();
-  finish();
-}
-
-let startTests = Task.async(function*() {
-  populateTree();
-  yield testKeyboardInteraction();
-  endTests();
 });
 
-function populateTree() {
+function populateTree(tree, doc) {
   tree.add([{
     id: "level1",
     label: "Level 1"
   }, {
     id: "level2-1",
     label: "Level 2"
   }, {
     id: "level3-1",
@@ -82,23 +71,24 @@ function populateTree() {
     attachment: {
       foo: "bar"
     }
   }]);
 }
 
 // Sends a click event on the passed DOM node in an async manner
 function click(node) {
-  executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, content));
+  let win = node.ownerDocument.defaultView;
+  executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, win));
 }
 
 /**
  * Tests if pressing navigation keys on the tree items does the expected behavior
  */
-let testKeyboardInteraction = Task.async(function*() {
+function* testKeyboardInteraction(tree, win) {
   info("Testing keyboard interaction with the tree");
   let event;
   let pass = (e, d, a) => event.resolve([e, d, a]);
 
   info("clicking on first top level item");
   let node = tree.root.children.firstChild.firstChild;
   event = Promise.defer();
   tree.once("select", pass);
@@ -107,132 +97,132 @@ let testKeyboardInteraction = Task.async
   node = tree.root.children.firstChild.nextSibling.firstChild;
   // node should not have selected class
   ok(!node.classList.contains("theme-selected"), "Node should not have selected class");
   ok(!node.hasAttribute("expanded"), "Node is not expanded");
 
   info("Pressing down key to select next item");
   event = Promise.defer();
   tree.once("select", pass);
-  EventUtils.sendKey("DOWN", content);
+  EventUtils.sendKey("DOWN", win);
   let [name, data, attachment] = yield event.promise;
   is(name, "select", "Select event was fired after pressing down");
   is(data[0], "level1", "Correct item was selected after pressing down");
   ok(!attachment, "null attachment was emitted");
   ok(node.classList.contains("theme-selected"), "Node has selected class");
   ok(node.hasAttribute("expanded"), "Node is expanded now");
 
   info("Pressing down key again to select next item");
   event = Promise.defer();
   tree.once("select", pass);
-  EventUtils.sendKey("DOWN", content);
+  EventUtils.sendKey("DOWN", win);
   [name, data, attachment] = yield event.promise;
   is(data.length, 2, "Correct level item was selected after second down keypress");
   is(data[0], "level1", "Correct parent level");
   is(data[1], "level2", "Correct second level");
 
   info("Pressing down key again to select next item");
   event = Promise.defer();
   tree.once("select", pass);
-  EventUtils.sendKey("DOWN", content);
+  EventUtils.sendKey("DOWN", win);
   [name, data, attachment] = yield event.promise;
   is(data.length, 3, "Correct level item was selected after third down keypress");
   is(data[0], "level1", "Correct parent level");
   is(data[1], "level2", "Correct second level");
   is(data[2], "level3", "Correct third level");
 
   info("Pressing down key again to select next item");
   event = Promise.defer();
   tree.once("select", pass);
-  EventUtils.sendKey("DOWN", content);
+  EventUtils.sendKey("DOWN", win);
   [name, data, attachment] = yield event.promise;
   is(data.length, 2, "Correct level item was selected after fourth down keypress");
   is(data[0], "level1", "Correct parent level");
   is(data[1], "level2-1", "Correct second level");
 
   // pressing left to check expand collapse feature.
   // This does not emit any event, so listening for keypress
   tree.root.children.addEventListener("keypress", function onClick() {
     tree.root.children.removeEventListener("keypress", onClick);
     // executeSoon so that other listeners on the same method are executed first
     executeSoon(() => event.resolve(null));
   });
   info("Pressing left key to collapse the item");
   event = Promise.defer();
   node = tree._selectedLabel;
   ok(node.hasAttribute("expanded"), "Item is expanded before left keypress");
-  EventUtils.sendKey("LEFT", content);
+  EventUtils.sendKey("LEFT", win);
   yield event.promise;
 
   ok(!node.hasAttribute("expanded"), "Item is not expanded after left keypress");
 
   // pressing left on collapsed item should select the previous item
 
   info("Pressing left key on collapsed item to select previous");
   tree.once("select", pass);
   event = Promise.defer();
   // parent node should have no effect of this keypress
   node = tree.root.children.firstChild.nextSibling.firstChild;
   ok(node.hasAttribute("expanded"), "Parent is expanded");
-  EventUtils.sendKey("LEFT", content);
+  EventUtils.sendKey("LEFT", win);
   [name, data] = yield event.promise;
   is(data.length, 3, "Correct level item was selected after second left keypress");
   is(data[0], "level1", "Correct parent level");
   is(data[1], "level2", "Correct second level");
   is(data[2], "level3", "Correct third level");
   ok(node.hasAttribute("expanded"), "Parent is still expanded after left keypress");
 
   // pressing down again
 
   info("Pressing down key to select next item");
   event = Promise.defer();
   tree.once("select", pass);
-  EventUtils.sendKey("DOWN", content);
+  EventUtils.sendKey("DOWN", win);
   [name, data, attachment] = yield event.promise;
   is(data.length, 2, "Correct level item was selected after fifth down keypress");
   is(data[0], "level1", "Correct parent level");
   is(data[1], "level2-1", "Correct second level");
 
   // collapsing the item to check expand feature.
 
   tree.root.children.addEventListener("keypress", function onClick() {
     tree.root.children.removeEventListener("keypress", onClick);
     executeSoon(() => event.resolve(null));
   });
   info("Pressing left key to collapse the item");
   event = Promise.defer();
   node = tree._selectedLabel;
   ok(node.hasAttribute("expanded"), "Item is expanded before left keypress");
-  EventUtils.sendKey("LEFT", content);
+  EventUtils.sendKey("LEFT", win);
   yield event.promise;
   ok(!node.hasAttribute("expanded"), "Item is collapsed after left keypress");
 
   // pressing right should expand this now.
 
   tree.root.children.addEventListener("keypress", function onClick() {
     tree.root.children.removeEventListener("keypress", onClick);
     executeSoon(() => event.resolve(null));
   });
   info("Pressing right key to expend the collapsed item");
   event = Promise.defer();
   node = tree._selectedLabel;
   ok(!node.hasAttribute("expanded"), "Item is collapsed before right keypress");
-  EventUtils.sendKey("RIGHT", content);
+  EventUtils.sendKey("RIGHT", win);
   yield event.promise;
   ok(node.hasAttribute("expanded"), "Item is expanded after right keypress");
 
   // selecting last item node to test edge navigation case
 
   tree.selectedItem = ["level1.1", "level2", "level3"];
   node = tree._selectedLabel;
   // pressing down again should not change selection
   event = Promise.defer();
   tree.root.children.addEventListener("keypress", function onClick() {
     tree.root.children.removeEventListener("keypress", onClick);
     executeSoon(() => event.resolve(null));
   });
   info("Pressing down key on last item of the tree");
-  EventUtils.sendKey("DOWN", content);
+  EventUtils.sendKey("DOWN", win);
   yield event.promise;
 
   ok(tree.isSelected(["level1.1", "level2", "level3"]),
      "Last item is still selected after pressing down on last item of the tree");
-});
+}
--- a/browser/devtools/shared/test/browser_treeWidget_mouse_interaction.js
+++ b/browser/devtools/shared/test/browser_treeWidget_mouse_interaction.js
@@ -4,46 +4,35 @@
 
 // Tests that mouse interaction works fine with tree widget
 
 const TEST_URI = "data:text/html;charset=utf-8,<head><link rel='stylesheet' " +
   "type='text/css' href='chrome://browser/skin/devtools/common.css'><link " +
   "rel='stylesheet' type='text/css' href='chrome://browser/skin/devtools/widg" +
   "ets.css'></head><body><div></div><span></span></body>";
 const {TreeWidget} = devtools.require("devtools/shared/widgets/TreeWidget");
-let {Task} = devtools.require("resource://gre/modules/Task.jsm");
-let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
-
-let doc, tree;
+const {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
 
-function test() {
-  waitForExplicitFinish();
-  addTab(TEST_URI, () => {
-    doc = content.document;
-    tree = new TreeWidget(doc.querySelector("div"), {
-      defaultType: "store"
-    });
-    startTests();
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+  let tree = new TreeWidget(doc.querySelector("div"), {
+    defaultType: "store"
   });
-}
 
-function endTests() {
+  populateTree(tree, doc);
+  yield testMouseInteraction(tree);
+
   tree.destroy();
-  doc = tree = null;
+  host.destroy();
   gBrowser.removeCurrentTab();
-  finish();
-}
-
-let startTests = Task.async(function*() {
-  populateTree();
-  yield testMouseInteraction();
-  endTests();
 });
 
-function populateTree() {
+function populateTree(tree, doc) {
   tree.add([{
     id: "level1",
     label: "Level 1"
   }, {
     id: "level2-1",
     label: "Level 2"
   }, {
     id: "level3-1",
@@ -82,23 +71,24 @@ function populateTree() {
     attachment: {
       foo: "bar"
     }
   }]);
 }
 
 // Sends a click event on the passed DOM node in an async manner
 function click(node) {
-  executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, content));
+  let win = node.ownerDocument.defaultView;
+  executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, win));
 }
 
 /**
  * Tests if clicking the tree items does the expected behavior
  */
-let testMouseInteraction = Task.async(function*() {
+function* testMouseInteraction(tree) {
   info("Testing mouse interaction with the tree");
   let event;
   let pass = (e, d, a) => event.resolve([e, d, a]);
 
   ok(!tree.selectedItem, "Nothing should be selected beforehand");
 
   tree.once("select", pass);
   let node = tree.root.children.firstChild.firstChild;
@@ -139,9 +129,9 @@ let testMouseInteraction = Task.async(fu
   event = Promise.defer();
   node2.addEventListener("click", function onClick() {
     node2.removeEventListener("click", onClick);
     executeSoon(() => event.resolve(null));
   });
   click(node2);
   yield event.promise;
   ok(!node2.hasAttribute("expanded"), "New node collapsed after click again");
-});
+}
--- a/browser/devtools/shared/test/head.js
+++ b/browser/devtools/shared/test/head.js
@@ -1,15 +1,18 @@
 /* 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/. */
 
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let {TargetFactory, require} = devtools;
 let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
-let TargetFactory = devtools.TargetFactory;
+let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+const {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+const {Hosts} = require("devtools/framework/toolbox-hosts");
 
 gDevTools.testing = true;
 SimpleTest.registerCleanupFunction(() => {
   gDevTools.testing = false;
 });
 
 const TEST_URI_ROOT = "http://example.com/browser/browser/devtools/shared/test/";
 const OPTIONS_VIEW_URL = TEST_URI_ROOT + "doc_options-view.xul";
@@ -129,20 +132,110 @@ function waitForValue(aOptions)
 function oneTimeObserve(name, callback) {
   var func = function() {
     Services.obs.removeObserver(func, name);
     callback();
   };
   Services.obs.addObserver(func, name, false);
 }
 
-function* createHost(type = "bottom", src = "data:text/html;charset=utf-8,") {
+let createHost = Task.async(function*(type = "bottom", src = "data:text/html;charset=utf-8,") {
   let host = new Hosts[type](gBrowser.selectedTab);
   let iframe = yield host.create();
 
   yield new Promise(resolve => {
     let domHelper = new DOMHelpers(iframe.contentWindow);
     iframe.setAttribute("src", src);
     domHelper.onceDOMReady(resolve);
   });
 
   return [host, iframe.contentWindow, iframe.contentDocument];
+});
+
+/**
+ * Load the Telemetry utils, then stub Telemetry.prototype.log in order to
+ * record everything that's logged in it.
+ * Store all recordings on Telemetry.telemetryInfo.
+ * @return {Telemetry}
+ */
+function loadTelemetryAndRecordLogs() {
+  info("Mock the Telemetry log function to record logged information");
+
+  let Telemetry = require("devtools/shared/telemetry");
+  Telemetry.prototype.telemetryInfo = {};
+  Telemetry.prototype._oldlog = Telemetry.prototype.log;
+  Telemetry.prototype.log = function(histogramId, value) {
+    if (!this.telemetryInfo) {
+      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
+      return;
+    }
+    if (histogramId) {
+      if (!this.telemetryInfo[histogramId]) {
+        this.telemetryInfo[histogramId] = [];
+      }
+
+      this.telemetryInfo[histogramId].push(value);
+    }
+  };
+
+  return Telemetry;
 }
+
+/**
+ * Stop recording the Telemetry logs and put back the utils as it was before.
+ */
+function stopRecordingTelemetryLogs(Telemetry) {
+  Telemetry.prototype.log = Telemetry.prototype._oldlog;
+  delete Telemetry.prototype._oldlog;
+  delete Telemetry.prototype.telemetryInfo;
+}
+
+/**
+ * Check the correctness of the data recorded in Telemetry after
+ * loadTelemetryAndRecordLogs was called.
+ */
+function checkTelemetryResults(Telemetry) {
+  let result = Telemetry.prototype.telemetryInfo;
+
+  for (let [histId, value] of Iterator(result)) {
+    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+      ok(value.length === 1 && value[0] === true,
+         "Per user value " + histId + " has a single value of true");
+    } else if (histId.endsWith("OPENED_BOOLEAN")) {
+      ok(value.length > 1, histId + " has more than one entry");
+
+      let okay = value.every(function(element) {
+        return element === true;
+      });
+
+      ok(okay, "All " + histId + " entries are === true");
+    } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+      ok(value.length > 1, histId + " has more than one entry");
+
+      let okay = value.every(function(element) {
+        return element > 0;
+      });
+
+      ok(okay, "All " + histId + " entries have time > 0");
+    }
+  }
+}
+
+/**
+ * Open and close the toolbox in the current browser tab, several times, waiting
+ * some amount of time in between.
+ * @param {Number} nbOfTimes
+ * @param {Number} usageTime in milliseconds
+ * @param {String} toolId
+ */
+function* openAndCloseToolbox(nbOfTimes, usageTime, toolId) {
+  for (let i = 0; i < nbOfTimes; i ++) {
+    info("Opening toolbox " + (i + 1));
+    let target = TargetFactory.forTab(gBrowser.selectedTab);
+    yield gDevTools.showToolbox(target, toolId)
+
+    // We use a timeout to check the toolbox's active time
+    yield new Promise(resolve => setTimeout(resolve, usageTime));
+
+    info("Closing toolbox " + (i + 1));
+    yield gDevTools.closeToolbox(target);
+  }
+}
--- a/browser/devtools/webconsole/console-output.js
+++ b/browser/devtools/webconsole/console-output.js
@@ -3097,128 +3097,126 @@ Widgets.ObjectRenderers.add({
     let closeTag = this.el("span.cm-tag");
     closeTag.textContent = ">";
     this.element.appendChild(closeTag);
 
     // Register this widget in the owner message so that it gets destroyed when
     // the message is destroyed.
     this.message.widgets.add(this);
 
-    this.linkToInspector();
+    this.linkToInspector().then(null, Cu.reportError);
   },
 
   /**
    * If the DOMNode being rendered can be highlit in the page, this function
    * will attach mouseover/out event listeners to do so, and the inspector icon
    * to open the node in the inspector.
-   * @return a promise (always the same) that resolves when the node has been
-   * linked to the inspector, or rejects if it wasn't (either if no toolbox
-   * could be found to access the inspector, or if the node isn't present in the
-   * inspector, i.e. if the node is in a DocumentFragment or not part of the
-   * tree, or not of type Ci.nsIDOMNode.ELEMENT_NODE).
+   * @return a promise that resolves when the node has been linked to the
+   * inspector, or rejects if it wasn't (either if no toolbox could be found to
+   * access the inspector, or if the node isn't present in the inspector, i.e.
+   * if the node is in a DocumentFragment or not part of the tree, or not of
+   * type Ci.nsIDOMNode.ELEMENT_NODE).
    */
-  linkToInspector: function()
+  linkToInspector: Task.async(function*()
   {
     if (this._linkedToInspector) {
-      return this._linkedToInspector;
+      return;
+    }
+
+    // Checking the node type
+    if (this.objectActor.preview.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
+      throw new Error("The object cannot be linked to the inspector as it " +
+        "isn't an element node");
+    }
+
+    // Checking the presence of a toolbox
+    let target = this.message.output.toolboxTarget;
+    this.toolbox = gDevTools.getToolbox(target);
+    if (!this.toolbox) {
+      throw new Error("The object cannot be linked to the inspector without a " +
+        "toolbox");
     }
 
-    this._linkedToInspector = Task.spawn(function*() {
-      // Checking the node type
-      if (this.objectActor.preview.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
-        throw null;
-      }
-
-      // Checking the presence of a toolbox
-      let target = this.message.output.toolboxTarget;
-      this.toolbox = gDevTools.getToolbox(target);
-      if (!this.toolbox) {
-        throw null;
-      }
-
-      // Checking that the inspector supports the node
-      yield this.toolbox.initInspector();
-      this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor);
-      if (!this._nodeFront) {
-        throw null;
-      }
-
-      // At this stage, the message may have been cleared already
-      if (!this.document) {
-        throw null;
-      }
-
-      this.highlightDomNode = this.highlightDomNode.bind(this);
-      this.element.addEventListener("mouseover", this.highlightDomNode, false);
-      this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
-      this.element.addEventListener("mouseout", this.unhighlightDomNode, false);
-
-      this._openInspectorNode = this._anchor("", {
-        className: "open-inspector",
-        onClick: this.openNodeInInspector.bind(this)
-      });
-      this._openInspectorNode.title = l10n.getStr("openNodeInInspector");
-    }.bind(this));
-
-    return this._linkedToInspector;
-  },
+    // Checking that the inspector supports the node
+    yield this.toolbox.initInspector();
+    this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor);
+    if (!this._nodeFront) {
+      throw new Error("The object cannot be linked to the inspector, the " +
+        "corresponding nodeFront could not be found");
+    }
+
+    // At this stage, the message may have been cleared already
+    if (!this.document) {
+      throw new Error("The object cannot be linked to the inspector, the " +
+        "message was got cleared away");
+    }
+
+    this.highlightDomNode = this.highlightDomNode.bind(this);
+    this.element.addEventListener("mouseover", this.highlightDomNode, false);
+    this.unhighlightDomNode = this.unhighlightDomNode.bind(this);
+    this.element.addEventListener("mouseout", this.unhighlightDomNode, false);
+
+    this._openInspectorNode = this._anchor("", {
+      className: "open-inspector",
+      onClick: this.openNodeInInspector.bind(this)
+    });
+    this._openInspectorNode.title = l10n.getStr("openNodeInInspector");
+
+    this._linkedToInspector = true;
+  }),
 
   /**
    * Highlight the DOMNode corresponding to the ObjectActor in the page.
    * @return a promise that resolves when the node has been highlighted, or
    * rejects if the node cannot be highlighted (detached from the DOM)
    */
-  highlightDomNode: function()
+  highlightDomNode: Task.async(function*()
   {
-    return Task.spawn(function*() {
-      yield this.linkToInspector();
-      let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
-      if (isAttached) {
-        yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
-      } else {
-        throw null;
-      }
-    }.bind(this));
-  },
+    yield this.linkToInspector();
+    let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
+    if (isAttached) {
+      yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
+    } else {
+      throw null;
+    }
+  }),
 
   /**
    * Unhighlight a previously highlit node
    * @see highlightDomNode
    * @return a promise that resolves when the highlighter has been hidden
    */
   unhighlightDomNode: function()
   {
     return this.linkToInspector().then(() => {
       return this.toolbox.highlighterUtils.unhighlight();
-    });
+    }).then(null, Cu.reportError);
   },
 
   /**
    * Open the DOMNode corresponding to the ObjectActor in the inspector panel
    * @return a promise that resolves when the inspector has been switched to
    * and the node has been selected, or rejects if the node cannot be selected
    * (detached from the DOM). Note that in any case, the inspector panel will
    * be switched to.
    */
-  openNodeInInspector: function()
+  openNodeInInspector: Task.async(function*()
   {
-    return Task.spawn(function*() {
-      yield this.linkToInspector();
-      yield this.toolbox.selectTool("inspector");
-
-      let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
-      if (isAttached) {
-        let onReady = this.toolbox.inspector.once("inspector-updated");
-        yield this.toolbox.selection.setNodeFront(this._nodeFront, "console");
-        yield onReady;
-      } else {
-        throw null;
-      }
-    }.bind(this));
-  },
+    yield this.linkToInspector();
+    yield this.toolbox.selectTool("inspector");
+
+    let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront);
+    if (isAttached) {
+      let onReady = this.toolbox.inspector.once("inspector-updated");
+      yield this.toolbox.selection.setNodeFront(this._nodeFront, "console");
+      yield onReady;
+    } else {
+      throw null;
+    }
+  }),
 
   destroy: function()
   {
     if (this.toolbox && this._nodeFront) {
       this.element.removeEventListener("mouseover", this.highlightDomNode, false);
       this.element.removeEventListener("mouseout", this.unhighlightDomNode, false);
       this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, true);
       this.toolbox = null;
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
@@ -1,20 +1,13 @@
 /* vim: set ft=javascript 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/. */
 
-///////////////////
-//
-// Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed.
-//
-thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Protocol error (unknownError): TypeError: this.conn.getActor(...) is null");
-
 // Tests that the $0 console helper works as intended.
 
 let inspector, h1, outputNode;
 
 function createDocument() {
   let doc = content.document;
   let div = doc.createElement("div");
   h1 = doc.createElement("h1");
--- a/browser/devtools/webconsole/test/browser_webconsole_output_03.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_output_03.js
@@ -1,20 +1,13 @@
 /*
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
-///////////////////
-//
-// Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed. 
-//
-thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Protocol error (unknownError): TypeError: this.conn.getActor(...) is null");
-
 // Test the webconsole output for various types of objects.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-03.html";
 
 let inputTests = [
 
   // 0
   {
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -442,19 +442,18 @@ function WiFiRuntime(deviceName) {
 
 WiFiRuntime.prototype = {
   type: RuntimeTypes.WIFI,
   connect: function(connection) {
     let service = discovery.getRemoteService("devtools", this.deviceName);
     if (!service) {
       return promise.reject(new Error("Can't find device: " + this.name));
     }
-    connection.host = service.host;
-    connection.port = service.port;
-    connection.encryption = service.encryption;
+    connection.advertisement = service;
+    // TODO: Customize client authentication UX
     connection.connect();
     return promise.resolve();
   },
   get id() {
     return this.deviceName;
   },
   get name() {
     return this.deviceName;
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -133,16 +133,17 @@ XPCOMUtils.defineLazyGetter(this, "ALL_B
     "copy-button",
     "paste-button",
     "zoom-out-button",
     "zoom-reset-button",
     "zoom-in-button",
     "BMB_bookmarksPopup",
     "BMB_unsortedBookmarksPopup",
     "BMB_bookmarksToolbarPopup",
+    "search-go-button",
   ]
   return DEFAULT_ITEMS.concat(PALETTE_ITEMS)
                       .concat(SPECIAL_CASES);
 });
 
 const OTHER_MOUSEUP_MONITORED_ITEMS = [
   "PlacesChevron",
   "PlacesToolbarItems",
@@ -453,16 +454,23 @@ this.BrowserUITelemetry = {
     // being clicked.
     if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) {
       // Base case - we clicked directly on one of our built-in items,
       // and we can go ahead and register that click.
       this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button);
       return;
     }
 
+    // If not, we need to check if the item's anonid is in our list
+    // of built-in items to check.
+    if (ALL_BUILTIN_ITEMS.indexOf(item.getAttribute("anonid")) != -1) {
+      this._countMouseUpEvent("click-builtin-item", item.getAttribute("anonid"), aEvent.button);
+      return;
+    }
+
     // If not, we need to check if one of the ancestors of the clicked
     // item is in our list of built-in items to check.
     let candidate = getIDBasedOnFirstIDedAncestor(item);
     if (ALL_BUILTIN_ITEMS.indexOf(candidate) != -1) {
       this._countMouseUpEvent("click-builtin-item", candidate, aEvent.button);
     }
   },
 
--- a/mobile/android/base/GeckoProfile.java
+++ b/mobile/android/base/GeckoProfile.java
@@ -14,16 +14,17 @@ import java.nio.charset.Charset;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
+import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.LocalBrowserDB;
 import org.mozilla.gecko.db.StubBrowserDB;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.util.INIParser;
 import org.mozilla.gecko.util.INISection;
 
@@ -826,19 +827,24 @@ public final class GeckoProfile {
      */
     @RobocopTarget
     public void enqueueInitialization(final File profileDir) {
         Log.i(LOGTAG, "Enqueuing profile init.");
         final Context context = mApplicationContext;
 
         // Add everything when we're done loading the distribution.
         final Distribution distribution = Distribution.getInstance(context);
-        distribution.addOnDistributionReadyCallback(new Runnable() {
+        distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
             @Override
-            public void run() {
+            public void distributionNotFound() {
+                this.distributionFound(null);
+            }
+
+            @Override
+            public void distributionFound(Distribution distribution) {
                 Log.d(LOGTAG, "Running post-distribution task: bookmarks.");
 
                 final ContentResolver cr = context.getContentResolver();
 
                 // Because we are running in the background, we want to synchronize on the
                 // GeckoProfile instance so that we don't race with main thread operations
                 // such as locking/unlocking/removing the profile.
                 synchronized (GeckoProfile.this) {
@@ -848,15 +854,34 @@ public final class GeckoProfile {
                     }
 
                     // We pass the number of added bookmarks to ensure that the
                     // indices of the distribution and default bookmarks are
                     // contiguous. Because there are always at least as many
                     // bookmarks as there are favicons, we can also guarantee that
                     // the favicon IDs won't overlap.
                     final LocalBrowserDB db = new LocalBrowserDB(getName());
-                    final int offset = db.addDistributionBookmarks(cr, distribution, 0);
+                    final int offset = distribution == null ? 0 : db.addDistributionBookmarks(cr, distribution, 0);
                     db.addDefaultBookmarks(context, cr, offset);
                 }
             }
+
+            @Override
+            public void distributionArrivedLate(Distribution distribution) {
+                Log.d(LOGTAG, "Running late distribution task: bookmarks.");
+                // Recover as best we can.
+                synchronized (GeckoProfile.this) {
+                    // Skip initialization if the profile directory has been removed.
+                    if (!profileDir.exists()) {
+                        return;
+                    }
+
+                    final LocalBrowserDB db = new LocalBrowserDB(getName());
+                    // We assume we've been called very soon after startup, and so our offset
+                    // into "Mobile Bookmarks" is the number of bookmarks in the DB.
+                    final ContentResolver cr = context.getContentResolver();
+                    final int offset = db.getCount(cr, "bookmarks");
+                    db.addDistributionBookmarks(cr, distribution, offset);
+                }
+            }
         });
     }
 }
--- a/mobile/android/base/db/SuggestedSites.java
+++ b/mobile/android/base/db/SuggestedSites.java
@@ -283,27 +283,26 @@ public class SuggestedSites {
         }
     }
 
     private void maybeWaitForDistribution() {
         if (distribution == null) {
             return;
         }
 
-        distribution.addOnDistributionReadyCallback(new Runnable() {
+        distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
             @Override
-            public void run() {
-                Log.d(LOGTAG, "Running post-distribution task: suggested sites.");
-
+            public void distributionNotFound() {
                 // If distribution doesn't exist, simply continue to load
                 // suggested sites directly from resources. See refresh().
-                if (!distribution.exists()) {
-                    return;
-                }
+            }
 
+            @Override
+            public void distributionFound(Distribution distribution) {
+                Log.d(LOGTAG, "Running post-distribution task: suggested sites.");
                 // Merge suggested sites from distribution with the
                 // default ones. Distribution takes precedence.
                 Map<String, Site> sites = loadFromDistribution(distribution);
                 if (sites == null) {
                     sites = new LinkedHashMap<String, Site>();
                 }
                 sites.putAll(loadFromResource());
 
@@ -315,16 +314,21 @@ public class SuggestedSites {
                 synchronized (file) {
                     saveSites(file, sites);
                 }
 
                 // Then notify any active loaders about the changes.
                 final ContentResolver cr = context.getContentResolver();
                 cr.notifyChange(BrowserContract.SuggestedSites.CONTENT_URI, null);
             }
+
+            @Override
+            public void distributionArrivedLate(Distribution distribution) {
+                distributionFound(distribution);
+            }
         });
     }
 
     /**
      * Loads suggested sites from a distribution file either matching the
      * current locale or with the fallback locale (en-US).
      *
      * It's assumed that the given distribution instance is ready to be
--- a/mobile/android/base/distribution/Distribution.java
+++ b/mobile/android/base/distribution/Distribution.java
@@ -31,22 +31,24 @@ import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
 import javax.net.ssl.SSLException;
 
 import org.apache.http.protocol.HTTP;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.util.FileUtils;
+import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.app.Activity;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.os.SystemClock;
 import android.util.Log;
 
@@ -99,39 +101,63 @@ public class Distribution {
     private static final int CODE_CATEGORY_FETCH_SOCKET_ERROR = 11;
     private static final int CODE_CATEGORY_FETCH_SSL_ERROR = 12;
     private static final int CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE = 13;
     private static final int CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE = 14;
 
     // Corresponds to the high value in Histograms.json.
     private static final long MAX_DOWNLOAD_TIME_MSEC = 40000;    // 40 seconds.
 
-    // Wait just a little while for the system to send a referrer intent after install.
-    private static final long DELAY_WAIT_FOR_REFERRER_MSEC = 400;
+    // If this is true, ready callbacks that arrive after our state is initially determined
+    // will be queued for delayed running.
+    // This should only be the case on first run, when we're in STATE_NONE.
+    // Implicitly accessed from any non-UI threads via Distribution.doInit, but in practice only one
+    // will actually perform initialization, and "non-UI thread" really means "background thread".
+    private volatile boolean shouldDelayLateCallbacks = false;
 
+    /**
+     * These tasks can be queued to run when a distribution is available.
+     *
+     * If <code>distributionFound</code> is called, it will be the only call.
+     * If <code>distributionNotFound</code> is called, it might be followed by
+     * a call to <code>distributionArrivedLate</code>.
+     *
+     * When <code>distributionNotFound</code> is called,
+     * {@link org.mozilla.gecko.distribution.Distribution#exists()} will return
+     * false. In the other two callbacks, it will return true.
+     */
+    public interface ReadyCallback {
+        void distributionNotFound();
+        void distributionFound(Distribution distribution);
+        void distributionArrivedLate(Distribution distribution);
+    }
 
     /**
      * Used as a drop-off point for ReferrerReceiver. Checked when we process
      * first-run distribution.
      *
      * This is `protected` so that test code can clear it between runs.
      */
     @RobocopTarget
     protected static volatile ReferrerDescriptor referrer;
 
     private static Distribution instance;
 
     private final Context context;
     private final String packagePath;
     private final String prefsBranch;
 
-    private volatile int state = STATE_UNKNOWN;
+    volatile int state = STATE_UNKNOWN;
     private File distributionDir;
 
-    private final Queue<Runnable> onDistributionReady = new ConcurrentLinkedQueue<Runnable>();
+    private final Queue<ReadyCallback> onDistributionReady = new ConcurrentLinkedQueue<>();
+
+    // Callbacks in this queue have been invoked once as distributionNotFound.
+    // If they're invoked again, it'll be with distributionArrivedLate.
+    private final Queue<ReadyCallback> onLateReady = new ConcurrentLinkedQueue<>();
 
     /**
      * This is a little bit of a bad singleton, because in principle a Distribution
      * can be created with arbitrary paths. So we only have one path to get here, and
      * it uses the default arguments. Watch out if you're creating your own instances!
      */
     public static synchronized Distribution getInstance(Context context) {
         if (instance == null) {
@@ -241,19 +267,61 @@ public class Distribution {
     }
 
     /**
      * This method is called by ReferrerReceiver when we receive a post-install
      * notification from Google Play.
      *
      * @param ref a parsed referrer value from the store-supplied intent.
      */
-    public static void onReceivedReferrer(ReferrerDescriptor ref) {
+    public static void onReceivedReferrer(final Context context, final ReferrerDescriptor ref) {
         // Track the referrer object for distribution handling.
         referrer = ref;
+
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                final Distribution distribution = Distribution.getInstance(context);
+
+                // This will bail if we aren't delayed, or we already have a distribution.
+                distribution.processDelayedReferrer(ref);
+            }
+        });
+    }
+
+    /**
+     * Handle a referrer intent that arrives after first use of the distribution.
+     */
+    private void processDelayedReferrer(final ReferrerDescriptor ref) {
+        ThreadUtils.assertOnBackgroundThread();
+        if (state != STATE_NONE) {
+            return;
+        }
+
+        Log.i(LOGTAG, "Processing delayed referrer.");
+
+        if (!checkIntentDistribution(ref)) {
+            // Oh well. No sense keeping these tasks around.
+            this.onLateReady.clear();
+            return;
+        }
+
+        // Persist our new state.
+        this.state = STATE_SET;
+        getSharedPreferences().edit().putInt(getKeyName(), this.state).apply();
+
+        // Just in case this isn't empty but doInit has finished.
+        runReadyQueue();
+
+        // Now process any tasks that already ran while we were in STATE_NONE
+        // to tell them of our good news.
+        runLateReadyQueue();
+
+        // Make sure that changes to search defaults are applied immediately.
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Changed", ""));
     }
 
     /**
      * Helper to grab a file in the distribution directory.
      *
      * Returns null if there is no distribution directory or the file
      * doesn't exist. Ensures init first.
      */
@@ -339,83 +407,86 @@ public class Distribution {
      * @return true if we've set a distribution.
      */
     @RobocopTarget
     protected boolean doInit() {
         ThreadUtils.assertNotOnUiThread();
 
         // Bail if we've already tried to initialize the distribution, and
         // there wasn't one.
-        final SharedPreferences settings;
-        if (prefsBranch == null) {
-            settings = GeckoSharedPrefs.forApp(context);
-        } else {
-            settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE);
-        }
+        final SharedPreferences settings = getSharedPreferences();
 
-        String keyName = context.getPackageName() + ".distribution_state";
+        final String keyName = getKeyName();
         this.state = settings.getInt(keyName, STATE_UNKNOWN);
+
         if (this.state == STATE_NONE) {
             runReadyQueue();
             return false;
         }
 
         // We've done the work once; don't do it again.
         if (this.state == STATE_SET) {
             // Note that we don't compute the distribution directory.
             // Call `ensureDistributionDir` if you need it.
             runReadyQueue();
             return true;
         }
 
         // We try the install intent, then the APK, then the system directory.
         final boolean distributionSet =
-                checkIntentDistribution() ||
+                checkIntentDistribution(referrer) ||
                 checkAPKDistribution() ||
                 checkSystemDistribution();
 
+        // If this is our first run -- and thus we weren't already in STATE_NONE or STATE_SET above --
+        // and we didn't find a distribution already, then we should hold on to callbacks in case we
+        // get a late distribution.
+        this.shouldDelayLateCallbacks = !distributionSet;
         this.state = distributionSet ? STATE_SET : STATE_NONE;
         settings.edit().putInt(keyName, this.state).apply();
 
         runReadyQueue();
         return distributionSet;
     }
 
     /**
      * If applicable, download and select the distribution specified in
      * the referrer intent.
      *
      * @return true if a referrer-supplied distribution was selected.
      */
-    private boolean checkIntentDistribution() {
+    private boolean checkIntentDistribution(final ReferrerDescriptor referrer) {
         if (referrer == null) {
-            // Wait a predetermined time and try again.
-            // Just block the thread, because it's the simplest solution.
-            try {
-                Thread.sleep(DELAY_WAIT_FOR_REFERRER_MSEC);
-            } catch (InterruptedException e) {
-                // Good enough.
-            }
-            if (referrer == null) {
-                return false;
-            }
+            return false;
         }
 
         URI uri = getReferredDistribution(referrer);
         if (uri == null) {
             return false;
         }
 
         long start = SystemClock.uptimeMillis();
         Log.v(LOGTAG, "Downloading referred distribution: " + uri);
 
         try {
-            HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
+            final HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
 
-            connection.setRequestProperty(HTTP.USER_AGENT, GeckoAppShell.getGeckoInterface().getDefaultUAString());
+            // If the Search Activity starts, and we handle the referrer intent, this'll return
+            // null. Recover gracefully in this case.
+            final GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
+            final String ua;
+            if (geckoInterface == null) {
+                // Fall back to GeckoApp's default implementation.
+                ua = HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
+                                                AppConstants.USER_AGENT_FENNEC_MOBILE;
+            } else {
+                ua = geckoInterface.getDefaultUAString();
+            }
+
+            connection.setRequestProperty(HTTP.USER_AGENT, ua);
             connection.setRequestProperty("Accept", EXPECTED_CONTENT_TYPE);
 
             try {
                 final JarInputStream distro;
                 try {
                     distro = fetchDistribution(uri, connection);
                 } catch (Exception e) {
                     Log.e(LOGTAG, "Error fetching distribution from network.", e);
@@ -531,28 +602,16 @@ public class Distribution {
             Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SOCKET_ERROR);
             return;
         }
 
         Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION);
     }
 
     /**
-     * Execute tasks that wanted to run when we were done loading
-     * the distribution. These tasks are expected to call {@link #exists()}
-     * to find out whether there's a distribution or not.
-     */
-    private void runReadyQueue() {
-        Runnable task;
-        while ((task = onDistributionReady.poll()) != null) {
-            ThreadUtils.postToBackgroundThread(task);
-        }
-    }
-
-    /**
      * @return true if we copied files out of the APK. Sets distributionDir in that case.
      */
     private boolean checkAPKDistribution() {
         try {
             // First, try copying distribution files out of the APK.
             if (copyFiles()) {
                 // We always copy to the data dir, and we only copy files from
                 // a 'distribution' subdirectory. Track our dist dir now that
@@ -754,31 +813,101 @@ public class Distribution {
         return context.getApplicationInfo().dataDir;
     }
 
     private File getSystemDistributionDir() {
         return new File("/system/" + context.getPackageName() + "/distribution");
     }
 
     /**
-     * The provided <code>Runnable</code> will be queued for execution after
+     * The provided <code>ReadyCallback</code> will be queued for execution after
      * the distribution is ready, or queued for immediate execution if the
      * distribution has already been processed.
      *
-     * Each <code>Runnable</code> will be executed on the background thread.
+     * Each <code>ReadyCallback</code> will be executed on the background thread.
      */
-    public void addOnDistributionReadyCallback(Runnable runnable) {
+    public void addOnDistributionReadyCallback(final ReadyCallback callback) {
         if (state == STATE_UNKNOWN) {
-            this.onDistributionReady.add(runnable);
+            // Queue for later.
+            onDistributionReady.add(callback);
         } else {
-            // If we're already initialized, just queue up the runnable.
-            ThreadUtils.postToBackgroundThread(runnable);
+            invokeCallbackDelayed(callback);
+        }
+    }
+
+    /**
+     * Run our delayed queue, after a delayed distribution arrives.
+     */
+    private void runLateReadyQueue() {
+        ReadyCallback task;
+        while ((task = onLateReady.poll()) != null) {
+            invokeLateCallbackDelayed(task);
+        }
+    }
+
+    /**
+     * Execute tasks that wanted to run when we were done loading
+     * the distribution.
+     */
+    private void runReadyQueue() {
+        ReadyCallback task;
+        while ((task = onDistributionReady.poll()) != null) {
+            invokeCallbackDelayed(task);
         }
     }
 
+    private void invokeLateCallbackDelayed(final ReadyCallback callback) {
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                // Sanity.
+                if (state != STATE_SET) {
+                    Log.w(LOGTAG, "Refusing to invoke late distro callback in state " + state);
+                    return;
+                }
+                callback.distributionArrivedLate(Distribution.this);
+            }
+        });
+    }
+
+    private void invokeCallbackDelayed(final ReadyCallback callback) {
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                switch (state) {
+                    case STATE_SET:
+                        callback.distributionFound(Distribution.this);
+                        break;
+                    case STATE_NONE:
+                        callback.distributionNotFound();
+                        if (shouldDelayLateCallbacks) {
+                            onLateReady.add(callback);
+                        }
+                        break;
+                    default:
+                        throw new IllegalStateException("Expected STATE_NONE or STATE_SET, got " + state);
+                }
+            }
+        });
+    }
+
     /**
      * A safe way for callers to determine if this Distribution instance
      * represents a real live distribution.
      */
     public boolean exists() {
         return state == STATE_SET;
     }
+
+    private String getKeyName() {
+        return context.getPackageName() + ".distribution_state";
+    }
+
+    private SharedPreferences getSharedPreferences() {
+        final SharedPreferences settings;
+        if (prefsBranch == null) {
+            settings = GeckoSharedPrefs.forApp(context);
+        } else {
+            settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE);
+        }
+        return settings;
+    }
 }
--- a/mobile/android/base/distribution/ReferrerReceiver.java
+++ b/mobile/android/base/distribution/ReferrerReceiver.java
@@ -44,17 +44,17 @@ public class ReferrerReceiver extends Br
             // This should never happen.
             return;
         }
 
         ReferrerDescriptor referrer = new ReferrerDescriptor(intent.getStringExtra("referrer"));
 
         // Track the referrer object for distribution handling.
         if (TextUtils.equals(referrer.campaign, DISTRIBUTION_UTM_CAMPAIGN)) {
-            Distribution.onReceivedReferrer(referrer);
+            Distribution.onReceivedReferrer(context, referrer);
         } else {
             Log.d(LOGTAG, "Not downloading distribution: non-matching campaign.");
         }
 
         // If this is a Mozilla campaign, pass the campaign along to Gecko.
         if (TextUtils.equals(referrer.source, MOZILLA_UTM_SOURCE)) {
             propagateMozillaCampaign(referrer);
         }
--- a/mobile/android/base/health/BrowserHealthRecorder.java
+++ b/mobile/android/base/health/BrowserHealthRecorder.java
@@ -526,27 +526,60 @@ public class BrowserHealthRecorder imple
         this.profileCache.beginInitialization();
         this.profileCache.setProfileCreationTime(getAndPersistProfileInitTime(context, profilePath));
         this.profileCache.setOSLocale(osLocale);
         this.profileCache.setAppLocale(appLocale);
 
         // Because the distribution lookup can take some time, do it at the end of
         // our background startup work, along with the Gecko snapshot fetch.
         final Distribution distribution = Distribution.getInstance(context);
-        distribution.addOnDistributionReadyCallback(new Runnable() {
+        distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
+            private void requestGeckoFields() {
+                Log.d(LOG_TAG, "Requesting all add-ons and FHR prefs from Gecko.");
+                dispatcher.registerGeckoThreadListener(BrowserHealthRecorder.this, EVENT_SNAPSHOT);
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HealthReport:RequestSnapshot", null));
+            }
+
             @Override
-            public void run() {
+            public void distributionNotFound() {
+                requestGeckoFields();
+            }
+
+            @Override
+            public void distributionFound(Distribution distribution) {
                 Log.d(LOG_TAG, "Running post-distribution task: health recorder.");
                 final DistributionDescriptor desc = distribution.getDescriptor();
                 if (desc != null && desc.valid) {
                     profileCache.setDistributionString(desc.id, desc.version);
                 }
-                Log.d(LOG_TAG, "Requesting all add-ons and FHR prefs from Gecko.");
-                dispatcher.registerGeckoThreadListener(BrowserHealthRecorder.this, EVENT_SNAPSHOT);
-                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HealthReport:RequestSnapshot", null));
+                requestGeckoFields();
+            }
+
+            @Override
+            public void distributionArrivedLate(Distribution distribution) {
+                profileCache.beginInitialization();
+
+                final DistributionDescriptor desc = distribution.getDescriptor();
+                if (desc != null && desc.valid) {
+                    profileCache.setDistributionString(desc.id, desc.version);
+                }
+
+                // Now rebuild.
+                try {
+                    profileCache.completeInitialization();
+
+                    if (state == State.INITIALIZING) {
+                        initializeStorage();
+                    } else {
+                        onEnvironmentChanged();
+                    }
+                } catch (Exception e) {
+                    // Well, we tried.
+                    Log.e(LOG_TAG, "Couldn't complete profile cache init.", e);
+                }
             }
         });
     }
 
     /**
      * Invoked in the background whenever the environment transitions between
      * two valid values.
      */
--- a/mobile/android/base/tests/testDistribution.java
+++ b/mobile/android/base/tests/testDistribution.java
@@ -168,17 +168,32 @@ public class testDistribution extends Co
         doTestInvalidReferrerIntent();
     }
 
     private void setOSLocale(Locale locale) {
         Locale.setDefault(locale);
         BrowserLocaleManager.storeAndNotifyOSLocale(GeckoSharedPrefs.forProfile(mActivity), locale);
     }
 
-    private void doReferrerTest(String ref, final TestableDistribution distribution, final Runnable distributionReady) throws InterruptedException {
+    private abstract class ExpectNoDistributionCallback implements Distribution.ReadyCallback {
+            @Override
+            public void distributionFound(final Distribution distribution) {
+                mAsserter.ok(false, "No distributionFound.", "Wasn't expecting a distribution!");
+                synchronized (distribution) {
+                    distribution.notifyAll();
+                }
+            }
+
+            @Override
+            public void distributionArrivedLate(final Distribution distribution) {
+                mAsserter.ok(false, "No distributionArrivedLate.", "Wasn't expecting a late distribution!");
+            }
+    }
+
+    private void doReferrerTest(String ref, final TestableDistribution distribution, final Distribution.ReadyCallback distributionReady) throws InterruptedException {
         final Intent intent = new Intent(ACTION_INSTALL_REFERRER);
         intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER);
         intent.putExtra("referrer", ref);
 
         final BroadcastReceiver receiver = new BroadcastReceiver() {
             @Override
             public void onReceive(Context context, Intent intent) {
                 Log.i(LOGTAG, "Test received " + intent.getAction());
@@ -210,20 +225,20 @@ public class testDistribution extends Co
 
     public void doTestValidReferrerIntent() throws Exception {
         // Equivalent to
         // am broadcast -a com.android.vending.INSTALL_REFERRER \
         //              -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \
         //              --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution"
         final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution";
         final TestableDistribution distribution = new TestableDistribution(mActivity);
-        final Runnable distributionReady = new Runnable() {
+        final Distribution.ReadyCallback distributionReady = new ExpectNoDistributionCallback() {
             @Override
-            public void run() {
-                Log.i(LOGTAG, "Test told distribution is ready.");
+            public void distributionNotFound() {
+                Log.i(LOGTAG, "Test told distribution processing is done.");
                 mAsserter.ok(!distribution.exists(), "Not processed.", "No download because we're offline.");
                 ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting();
                 mAsserter.dumpLog("Referrer was " + referrerValue);
                 mAsserter.is(referrerValue.content, "testcontent", "Referrer content");
                 mAsserter.is(referrerValue.medium, "testmedium", "Referrer medium");
                 mAsserter.is(referrerValue.campaign, "distribution", "Referrer campaign");
                 synchronized (distribution) {
                     distribution.notifyAll();
@@ -241,19 +256,19 @@ public class testDistribution extends Co
      */
     public void doTestInvalidReferrerIntent() throws Exception {
         // Equivalent to
         // am broadcast -a com.android.vending.INSTALL_REFERRER \
         //              -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \
         //              --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname"
         final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname";
         final TestableDistribution distribution = new TestableDistribution(mActivity);
-        final Runnable distributionReady = new Runnable() {
+        final Distribution.ReadyCallback distributionReady = new ExpectNoDistributionCallback() {
             @Override
-            public void run() {
+            public void distributionNotFound() {
                 mAsserter.ok(!distribution.exists(), "Not processed.", "No download because campaign was wrong.");
                 ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting();
                 mAsserter.is(referrerValue, null, "No referrer.");
                 synchronized (distribution) {
                     distribution.notifyAll();
                 }
             }
         };
--- a/mobile/android/base/widget/ActivityChooserModel.java
+++ b/mobile/android/base/widget/ActivityChooserModel.java
@@ -1044,41 +1044,44 @@ public class ActivityChooserModel extend
             if (!f.exists()) {
                 // Fall back to the non-profile aware file if it exists...
                 File oldFile = new File(mHistoryFileName);
                 oldFile.renameTo(f);
             }
             readHistoricalDataFromStream(new FileInputStream(f));
         } catch (FileNotFoundException fnfe) {
             final Distribution dist = Distribution.getInstance(mContext);
-            dist.addOnDistributionReadyCallback(new Runnable() {
+            dist.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
                 @Override
-                public void run() {
-                    Log.d(LOGTAG, "Running post-distribution task: quickshare.");
+                public void distributionNotFound() {
+                }
 
-                    if (!dist.exists()) {
-                        return;
-                    }
-
+                @Override
+                public void distributionFound(Distribution distribution) {
                     try {
                         File distFile = dist.getDistributionFile("quickshare/" + mHistoryFileName);
                         if (distFile == null) {
                             if (DEBUG) {
                                 Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
                             }
                             return;
                         }
                         readHistoricalDataFromStream(new FileInputStream(distFile));
                     } catch (Exception ex) {
                         if (DEBUG) {
                             Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
                         }
                         return;
                     }
                 }
+
+                @Override
+                public void distributionArrivedLate(Distribution distribution) {
+                    distributionFound(distribution);
+                }
             });
         }
     }
 
     void readHistoricalDataFromStream(FileInputStream fis) {
         try {
             XmlPullParser parser = Xml.newPullParser();
             parser.setInput(fis, null);
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -7297,17 +7297,18 @@ var RemoteDebugger = {
   _isEnabled: function rd_isEnabled() {
     return Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
   },
 
   /**
    * Prompt the user to accept or decline the incoming connection.
    * This is passed to DebuggerService.init as a callback.
    *
-   * @return true if the connection should be permitted, false otherwise
+   * @return An AuthenticationResult value.
+   *         A promise that will be resolved to the above is also allowed.
    */
   _showConnectionPrompt: function rd_showConnectionPrompt() {
     let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle");
     let msg = Strings.browser.GetStringFromName("remoteIncomingPromptMessage");
     let disable = Strings.browser.GetStringFromName("remoteIncomingPromptDisable");
     let cancel = Strings.browser.GetStringFromName("remoteIncomingPromptCancel");
     let agree = Strings.browser.GetStringFromName("remoteIncomingPromptAccept");
 
@@ -7329,22 +7330,21 @@ var RemoteDebugger = {
     });
 
     // Spin this thread while we wait for a result.
     let thread = Services.tm.currentThread;
     while (result == null)
       thread.processNextEvent(true);
 
     if (result === 0)
-      return true;
+      return DebuggerServer.AuthenticationResult.ALLOW;
     if (result === 2) {
-      Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
-      this._stop();
-    }
-    return false;
+      return DebuggerServer.AuthenticationResult.DISABLE_ALL;
+    }
+    return DebuggerServer.AuthenticationResult.DENY;
   },
 
   _restart: function rd_restart() {
     this._stop();
     this._start();
   },
 
   _start: function rd_start() {
@@ -7353,19 +7353,22 @@ var RemoteDebugger = {
         DebuggerServer.init();
         DebuggerServer.addBrowserActors();
         DebuggerServer.registerModule("resource://gre/modules/dbg-browser-actors.js");
       }
 
       let pathOrPort = this._getPath();
       if (!pathOrPort)
         pathOrPort = this._getPort();
+      let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
+      let authenticator = new AuthenticatorType.Server();
+      authenticator.allowConnection = this._showConnectionPrompt.bind(this);
       let listener = DebuggerServer.createListener();
       listener.portOrPath = pathOrPort;
-      listener.allowConnection = this._showConnectionPrompt.bind(this);
+      listener.authenticator = authenticator;
       listener.open();
       dump("Remote debugger listening at path " + pathOrPort);
     } catch(e) {
       dump("Remote debugger didn't start: " + e);
     }
   },
 
   _stop: function rd_start() {
@@ -7526,29 +7529,39 @@ var ExternalApps = {
   },
 };
 
 var Distribution = {
   // File used to store campaign data
   _file: null,
 
   init: function dc_init() {
+    Services.obs.addObserver(this, "Distribution:Changed", false);
     Services.obs.addObserver(this, "Distribution:Set", false);
     Services.obs.addObserver(this, "prefservice:after-app-defaults", false);
     Services.obs.addObserver(this, "Campaign:Set", false);
 
     // Look for file outside the APK:
     // /data/data/org.mozilla.xxx/distribution.json
     this._file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
     this._file.append("distribution.json");
     this.readJSON(this._file, this.update);
   },
 
   observe: function dc_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
+      case "Distribution:Changed":
+        // Re-init the search service.
+        try {
+          Services.search._asyncReInit();
+        } catch (e) {
+          console.log("Unable to reinit search service.");
+        }
+        // Fall through.
+
       case "Distribution:Set":
         // Reload the default prefs so we can observe "prefservice:after-app-defaults"
         Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null);
         break;
 
       case "prefservice:after-app-defaults":
         this.getPrefs();
         break;
--- a/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java
+++ b/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java
@@ -62,19 +62,19 @@ public class SearchEngineManager impleme
         this.context = context;
         this.distribution = distribution;
         GeckoSharedPrefs.forApp(context).registerOnSharedPreferenceChangeListener(this);
     }
 
     /**
      * Sets a callback to be called when the default engine changes.
      *
-     * @param callback SearchEngineCallback to be called after the search engine
-     *                 changed. This will run on the UI thread.
-     *                 Note: callback may be called with null engine.
+     * @param changeCallback SearchEngineCallback to be called after the search engine
+     *                       changed. This will run on the UI thread.
+     *                       Note: callback may be called with null engine.
      */
     public void setChangeCallback(SearchEngineCallback changeCallback) {
         this.changeCallback = changeCallback;
     }
 
     /**
      * Perform an action with the user's default search engine.
      *
@@ -136,35 +136,67 @@ public class SearchEngineManager impleme
      * the distribution (if one exists), and finally fall back to the localized default.
      *
      * @param callback SearchEngineCallback to be called after successfully looking
      *                 up the search engine. This will run on the UI thread.
      *                 Note: callback may be called with null engine.
      */
     private void getDefaultEngine(final SearchEngineCallback callback) {
         // This runnable is posted to the background thread.
-        distribution.addOnDistributionReadyCallback(new Runnable() {
+        distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
+            @Override
+            public void distributionNotFound() {
+                defaultBehavior();
+            }
+
+            @Override
+            public void distributionFound(Distribution distribution) {
+                defaultBehavior();
+            }
+
             @Override
-            public void run() {
+            public void distributionArrivedLate(Distribution distribution) {
+                // Let's see if there's a name in the distro.
+                // If so, just this once we'll override the saved value.
+                final String name = getDefaultEngineNameFromDistribution();
+
+                if (name == null) {
+                    return;
+                }
+
+                // Store the default engine name for the future.
+                // Increment an 'ignore' counter so that this preference change
+                // won't cause getDefaultEngine to be called again.
+                ignorePreferenceChange++;
+                GeckoSharedPrefs.forApp(context)
+                        .edit()
+                        .putString(PREF_DEFAULT_ENGINE_KEY, name)
+                        .apply();
+
+                final SearchEngine engine = createEngineFromName(name);
+                runCallback(engine, callback);
+            }
+
+            private void defaultBehavior() {
                 // First look for a default name stored in shared preferences.
                 String name = GeckoSharedPrefs.forApp(context).getString(PREF_DEFAULT_ENGINE_KEY, null);
 
                 if (name != null) {
                     Log.d(LOG_TAG, "Found default engine name in SharedPreferences: " + name);
                 } else {
                     // First, look for the default search engine in a distribution.
                     name = getDefaultEngineNameFromDistribution();
                     if (name == null) {
                         // Otherwise, get the default engine that we ship.
                         name = getDefaultEngineNameFromLocale();
                     }
 
                     // Store the default engine name for the future.
                     // Increment an 'ignore' counter so that this preference change
-                    // won'tcause getDefaultEngine to be called again.
+                    // won't cause getDefaultEngine to be called again.
                     ignorePreferenceChange++;
                     GeckoSharedPrefs.forApp(context)
                                     .edit()
                                     .putString(PREF_DEFAULT_ENGINE_KEY, name)
                                     .apply();
                 }
 
                 final SearchEngine engine = createEngineFromName(name);
--- a/testing/xpcshell/head.js
+++ b/testing/xpcshell/head.js
@@ -432,19 +432,25 @@ function _initDebugging(port) {
   do_print("*******************************************************************");
   do_print("Waiting for the debugger to connect on port " + port)
   do_print("")
   do_print("To connect the debugger, open a Firefox instance, select 'Connect'");
   do_print("from the Developer menu and specify the port as " + port);
   do_print("*******************************************************************");
   do_print("")
 
+  let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
+  let authenticator = new AuthenticatorType.Server();
+  authenticator.allowConnection = () => {
+    return DebuggerServer.AuthenticationResult.ALLOW;
+  };
+
   let listener = DebuggerServer.createListener();
   listener.portOrPath = port;
-  listener.allowConnection = () => true;
+  listener.authenticator = authenticator;
   listener.open();
 
   // spin an event loop until the debugger connects.
   let thr = Components.classes["@mozilla.org/thread-manager;1"]
               .getService().currentThread;
   while (!initialized) {
     do_print("Still waiting for debugger to connect...");
     thr.processNextEvent(true);
--- a/toolkit/devtools/client/connection-manager.js
+++ b/toolkit/devtools/client/connection-manager.js
@@ -48,16 +48,21 @@ DevToolsUtils.defineLazyModuleGetter(thi
  *
  * Properties:
  *  . host                  IP address or hostname
  *  . port                  Port
  *  . logs                  Current logs. "newlog" event notifies new available logs
  *  . store                 Reference to a local data store (see below)
  *  . keepConnecting        Should the connection keep trying to connect?
  *  . encryption            Should the connection be encrypted?
+ *  . authentication        What authentication scheme should be used?
+ *  . authenticator         The |Authenticator| instance used.  Overriding
+ *                          properties of this instance may be useful to
+ *                          customize authentication UX for a specific use case.
+ *  . advertisement         The server's advertisement if found by discovery
  *  . status                Connection status:
  *                            Connection.Status.CONNECTED
  *                            Connection.Status.DISCONNECTED
  *                            Connection.Status.CONNECTING
  *                            Connection.Status.DISCONNECTING
  *                            Connection.Status.DESTROYED
  *
  * Events (as in event-emitter.js):
@@ -174,19 +179,70 @@ Connection.prototype = {
 
   set port(value) {
     if (this._port && this._port == value)
       return;
     this._port = value;
     this.emit(Connection.Events.PORT_CHANGED);
   },
 
+  get authentication() {
+    return this._authentication;
+  },
+
+  set authentication(value) {
+    this._authentication = value;
+    // Create an |Authenticator| of this type
+    if (!value) {
+      this.authenticator = null;
+      return;
+    }
+    let AuthenticatorType = DebuggerClient.Authenticators.get(value);
+    this.authenticator = new AuthenticatorType.Client();
+  },
+
+  get advertisement() {
+    return this._advertisement;
+  },
+
+  set advertisement(advertisement) {
+    // The full advertisement may contain more info than just the standard keys
+    // below, so keep a copy for use during connection later.
+    this._advertisement = advertisement;
+    if (advertisement) {
+      ["host", "port", "encryption", "authentication"].forEach(key => {
+        this[key] = advertisement[key];
+      });
+    }
+  },
+
+  /**
+   * Settings to be passed to |socketConnect| at connection time.
+   */
+  get socketSettings() {
+    let settings = {};
+    if (this.advertisement) {
+      // Use the advertisement as starting point if it exists, as it may contain
+      // extra data, like the server's cert.
+      Object.assign(settings, this.advertisement);
+    }
+    Object.assign(settings, {
+      host: this.host,
+      port: this.port,
+      encryption: this.encryption,
+      authenticator: this.authenticator
+    });
+    return settings;
+  },
+
   resetOptions() {
     this.keepConnecting = false;
     this.encryption = false;
+    this.authentication = null;
+    this.advertisement = null;
   },
 
   disconnect: function(force) {
     if (this.status == Connection.Status.DESTROYED) {
       return;
     }
     clearTimeout(this._timeoutID);
     if (this.status == Connection.Status.CONNECTED ||
@@ -233,21 +289,18 @@ Connection.prototype = {
 
   _getTransport: Task.async(function*() {
     if (this._customTransport) {
       return this._customTransport;
     }
     if (!this.host) {
       return DebuggerServer.connectPipe();
     }
-    let transport = yield DebuggerClient.socketConnect({
-      host: this.host,
-      port: this.port,
-      encryption: this.encryption
-    });
+    let settings = this.socketSettings;
+    let transport = yield DebuggerClient.socketConnect(settings);
     return transport;
   }),
 
   _clientConnect: function () {
     this._getTransport().then(transport => {
       if (!transport) {
         return;
       }
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -78,16 +78,19 @@ function dumpv(msg) {
 let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
   .getService(Ci.mozIJSSubScriptLoader);
 loader.loadSubScript("resource://gre/modules/devtools/transport/transport.js", this);
 
 DevToolsUtils.defineLazyGetter(this, "DebuggerSocket", () => {
   let { DebuggerSocket } = devtools.require("devtools/toolkit/security/socket");
   return DebuggerSocket;
 });
+DevToolsUtils.defineLazyGetter(this, "Authentication", () => {
+  return devtools.require("devtools/toolkit/security/auth");
+});
 
 /**
  * TODO: Get rid of this API in favor of EventTarget (bug 1042642)
  *
  * Add simple event notification to a prototype object. Any object that has
  * some use for event notifications or the observer pattern in general can be
  * augmented with the necessary facilities by passing its prototype to this
  * function.
@@ -367,21 +370,27 @@ DebuggerClient.Argument = function (aPos
 
 DebuggerClient.Argument.prototype.getArgument = function (aParams) {
   if (!(this.position in aParams)) {
     throw new Error("Bad index into params: " + this.position);
   }
   return aParams[this.position];
 };
 
-// Expose this to save callers the trouble of importing DebuggerSocket
+// Expose these to save callers the trouble of importing DebuggerSocket
 DebuggerClient.socketConnect = function(options) {
   // Defined here instead of just copying the function to allow lazy-load
   return DebuggerSocket.connect(options);
 };
+DevToolsUtils.defineLazyGetter(DebuggerClient, "Authenticators", () => {
+  return Authentication.Authenticators;
+});
+DevToolsUtils.defineLazyGetter(DebuggerClient, "AuthenticationResult", () => {
+  return Authentication.AuthenticationResult;
+});
 
 DebuggerClient.prototype = {
   /**
    * Connect to the server and start exchanging protocol messages.
    *
    * @param aOnConnected function
    *        If specified, will be called when the greeting packet is
    *        received from the debugging server.
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/auth.js
@@ -0,0 +1,609 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript 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/. */
+
+"use strict";
+
+let { Ci, Cc } = require("chrome");
+let Services = require("Services");
+let promise = require("promise");
+let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
+let { dumpn, dumpv } = DevToolsUtils;
+loader.lazyRequireGetter(this, "prompt",
+  "devtools/toolkit/security/prompt");
+loader.lazyRequireGetter(this, "cert",
+  "devtools/toolkit/security/cert");
+DevToolsUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+
+/**
+ * A simple enum-like object with keys mirrored to values.
+ * This makes comparison to a specfic value simpler without having to repeat and
+ * mis-type the value.
+ */
+function createEnum(obj) {
+  for (let key in obj) {
+    obj[key] = key;
+  }
+  return obj;
+}
+
+/**
+ * |allowConnection| implementations can return various values as their |result|
+ * field to indicate what action to take.  By specifying these, we can
+ * centralize the common actions available, while still allowing embedders to
+ * present their UI in whatever way they choose.
+ */
+let AuthenticationResult = exports.AuthenticationResult = createEnum({
+
+  /**
+   * Close all listening sockets, and disable them from opening again.
+   */
+  DISABLE_ALL: null,
+
+  /**
+   * Deny the current connection.
+   */
+  DENY: null,
+
+  /**
+   * Additional data needs to be exchanged before a result can be determined.
+   */
+  PENDING: null,
+
+  /**
+   * Allow the current connection.
+   */
+  ALLOW: null,
+
+  /**
+   * Allow the current connection, and persist this choice for future
+   * connections from the same client.  This requires a trustable mechanism to
+   * identify the client in the future, such as the cert used during OOB_CERT.
+   */
+  ALLOW_PERSIST: null
+
+});
+
+/**
+ * An |Authenticator| implements an authentication mechanism via various hooks
+ * in the client and server debugger socket connection path (see socket.js).
+ *
+ * |Authenticator|s are stateless objects.  Each hook method is passed the state
+ * it needs by the client / server code in socket.js.
+ *
+ * Separate instances of the |Authenticator| are created for each use (client
+ * connection, server listener) in case some methods are customized by the
+ * embedder for a given use case.
+ */
+let Authenticators = {};
+
+/**
+ * The Prompt authenticator displays a server-side user prompt that includes
+ * connection details, and asks the user to verify the connection.  There are
+ * no cryptographic properties at work here, so it is up to the user to be sure
+ * that the client can be trusted.
+ */
+let Prompt = Authenticators.Prompt = {};
+
+Prompt.mode = "PROMPT";
+
+Prompt.Client = function() {};
+Prompt.Client.prototype = {
+
+  mode: Prompt.mode,
+
+  /**
+   * When client has just made a new socket connection, validate the connection
+   * to ensure it meets the authenticator's policies.
+   *
+   * @param host string
+   *        The host name or IP address of the debugger server.
+   * @param port number
+   *        The port number of the debugger server.
+   * @param encryption boolean (optional)
+   *        Whether the server requires encryption.  Defaults to false.
+   * @param cert object (optional)
+   *        The server's cert details.
+   * @param s nsISocketTransport
+   *        Underlying socket transport, in case more details are needed.
+   * @return boolean
+   *         Whether the connection is valid.
+   */
+  validateConnection() {
+    return true;
+  },
+
+  /**
+   * Work with the server to complete any additional steps required by this
+   * authenticator's policies.
+   *
+   * Debugging commences after this hook completes successfully.
+   *
+   * @param host string
+   *        The host name or IP address of the debugger server.
+   * @param port number
+   *        The port number of the debugger server.
+   * @param encryption boolean (optional)
+   *        Whether the server requires encryption.  Defaults to false.
+   * @param transport DebuggerTransport
+   *        A transport that can be used to communicate with the server.
+   * @return A promise can be used if there is async behavior.
+   */
+  authenticate() {},
+
+};
+
+Prompt.Server = function() {};
+Prompt.Server.prototype = {
+
+  mode: Prompt.mode,
+
+  /**
+   * Verify that listener settings are appropriate for this authentication mode.
+   *
+   * @param listener SocketListener
+   *        The socket listener about to be opened.
+   * @throws if validation requirements are not met
+   */
+  validateOptions() {},
+
+  /**
+   * Augment options on the listening socket about to be opened.
+   *
+   * @param listener SocketListener
+   *        The socket listener about to be opened.
+   * @param socket nsIServerSocket
+   *        The socket that is about to start listening.
+   */
+  augmentSocketOptions() {},
+
+  /**
+   * Augment the service discovery advertisement with any additional data needed
+   * to support this authentication mode.
+   *
+   * @param listener SocketListener
+   *        The socket listener that was just opened.
+   * @param advertisement object
+   *        The advertisement being built.
+   */
+  augmentAdvertisement(listener, advertisement) {
+    advertisement.authentication = Prompt.mode;
+  },
+
+  /**
+   * Determine whether a connection the server should be allowed or not based on
+   * this authenticator's policies.
+   *
+   * @param session object
+   *        In PROMPT mode, the |session| includes:
+   *        {
+   *          client: {
+   *            host,
+   *            port
+   *          },
+   *          server: {
+   *            host,
+   *            port
+   *          },
+   *          transport
+   *        }
+   * @return An AuthenticationResult value.
+   *         A promise that will be resolved to the above is also allowed.
+   */
+  authenticate({ client, server }) {
+    if (!Services.prefs.getBoolPref("devtools.debugger.prompt-connection")) {
+      return AuthenticationResult.ALLOW;
+    }
+    return this.allowConnection({
+      authentication: this.mode,
+      client,
+      server
+    });
+  },
+
+  /**
+   * Prompt the user to accept or decline the incoming connection. The default
+   * implementation is used unless this is overridden on a particular
+   * authenticator instance.
+   *
+   * It is expected that the implementation of |allowConnection| will show a
+   * prompt to the user so that they can allow or deny the connection.
+   *
+   * @param session object
+   *        In PROMPT mode, the |session| includes:
+   *        {
+   *          client: {
+   *            host,
+   *            port
+   *          },
+   *          server: {
+   *            host,
+   *            port
+   *          }
+   *        }
+   * @return An AuthenticationResult value.
+   *         A promise that will be resolved to the above is also allowed.
+   */
+  allowConnection: prompt.Server.defaultAllowConnection,
+
+};
+
+/**
+ * The out-of-band (OOB) cert authenticator is based on self-signed X.509 certs
+ * at both the client and server end.
+ *
+ * The user is first prompted to verify the connection, similar to the prompt
+ * method above.  This prompt may display cert fingerprints if desired.
+ *
+ * Assuming the user approves the connection, further UI is used to assist the
+ * user in tranferring out-of-band (OOB) verification of the client's
+ * certificate.  For example, this could take the form of a QR code that the
+ * client displays which is then scanned by a camera on the server.
+ *
+ * Since it is assumed that an attacker can't forge the client's X.509 cert, the
+ * user may also choose to always allow a client, which would permit immediate
+ * connections in the future with no user interaction needed.
+ *
+ * See docs/wifi.md for details of the authentication design.
+ */
+let OOBCert = Authenticators.OOBCert = {};
+
+OOBCert.mode = "OOB_CERT";
+
+OOBCert.Client = function() {};
+OOBCert.Client.prototype = {
+
+  mode: OOBCert.mode,
+
+  /**
+   * When client has just made a new socket connection, validate the connection
+   * to ensure it meets the authenticator's policies.
+   *
+   * @param host string
+   *        The host name or IP address of the debugger server.
+   * @param port number
+   *        The port number of the debugger server.
+   * @param encryption boolean (optional)
+   *        Whether the server requires encryption.  Defaults to false.
+   * @param cert object (optional)
+   *        The server's cert details.
+   * @param socket nsISocketTransport
+   *        Underlying socket transport, in case more details are needed.
+   * @return boolean
+   *         Whether the connection is valid.
+   */
+  validateConnection({ cert, socket }) {
+    // Step B.7
+    // Client verifies that Server's cert matches hash(ServerCert) from the
+    // advertisement
+    dumpv("Validate server cert hash");
+    let serverCert = socket.securityInfo.QueryInterface(Ci.nsISSLStatusProvider)
+                           .SSLStatus.serverCert;
+    let advertisedCert = cert;
+    if (serverCert.sha256Fingerprint != advertisedCert.sha256) {
+      dumpn("Server cert hash doesn't match advertisement");
+      return false;
+    }
+    return true;
+  },
+
+  /**
+   * Work with the server to complete any additional steps required by this
+   * authenticator's policies.
+   *
+   * Debugging commences after this hook completes successfully.
+   *
+   * @param host string
+   *        The host name or IP address of the debugger server.
+   * @param port number
+   *        The port number of the debugger server.
+   * @param encryption boolean (optional)
+   *        Whether the server requires encryption.  Defaults to false.
+   * @param cert object (optional)
+   *        The server's cert details.
+   * @param transport DebuggerTransport
+   *        A transport that can be used to communicate with the server.
+   * @return A promise can be used if there is async behavior.
+   */
+  authenticate({ host, port, cert, transport }) {
+    let deferred = promise.defer();
+    let oobData;
+
+    let activeSendDialog;
+    let closeDialog = () => {
+      // Close any prompts the client may have been showing from previous
+      // authentication steps
+      if (activeSendDialog && activeSendDialog.close) {
+        activeSendDialog.close();
+        activeSendDialog = null;
+      }
+    };
+
+    transport.hooks = {
+      onPacket: Task.async(function*(packet) {
+        closeDialog();
+        let { authResult } = packet;
+        switch (authResult) {
+          case AuthenticationResult.PENDING:
+            // Step B.8
+            // Client creates hash(ClientCert) + K(random 128-bit number)
+            oobData = yield this._createOOB();
+            activeSendDialog = this.sendOOB({
+              host,
+              port,
+              cert,
+              authResult,
+              oob: oobData
+            });
+            break;
+          case AuthenticationResult.ALLOW:
+          case AuthenticationResult.ALLOW_PERSIST:
+            // Step B.12
+            // Client verifies received value matches K
+            if (packet.k != oobData.k) {
+              transport.close(new Error("Auth secret mismatch"));
+              return;
+            }
+            // Step B.13
+            // Debugging begins
+            transport.hooks = null;
+            deferred.resolve(transport);
+            break;
+          default:
+            transport.close(new Error("Invalid auth result: " + authResult));
+            return;
+        }
+      }.bind(this)),
+      onClosed(reason) {
+        closeDialog();
+        // Transport died before auth completed
+        transport.hooks = null;
+        deferred.reject(reason);
+      }
+    };
+    transport.ready();
+    return deferred.promise;
+  },
+
+  /**
+   * Create the package of data that needs to be transferred across the OOB
+   * channel.
+   */
+  _createOOB: Task.async(function*() {
+    let clientCert = yield cert.local.getOrCreate();
+    return {
+      sha256: clientCert.sha256Fingerprint,
+      k: this._createRandom()
+    };
+  }),
+
+  _createRandom() {
+    const length = 16; // 16 bytes / 128 bits
+    let rng = Cc["@mozilla.org/security/random-generator;1"]
+              .createInstance(Ci.nsIRandomGenerator);
+    let bytes = rng.generateRandomBytes(length);
+    return [for (byte of bytes) byte.toString(16)].join("");
+  },
+
+  /**
+   * Send data across the OOB channel to the server to authenticate the devices.
+   *
+   * @param host string
+   *        The host name or IP address of the debugger server.
+   * @param port number
+   *        The port number of the debugger server.
+   * @param cert object (optional)
+   *        The server's cert details.
+   * @param authResult AuthenticationResult
+   *        Authentication result sent from the server.
+   * @param oob object (optional)
+   *        The token data to be transferred during OOB_CERT step 8:
+   *        * sha256: hash(ClientCert)
+   *        * k     : K(random 128-bit number)
+   * @return object containing:
+   *         * close: Function to hide the notification
+   */
+  sendOOB: prompt.Client.defaultSendOOB,
+
+};
+
+OOBCert.Server = function() {};
+OOBCert.Server.prototype = {
+
+  mode: OOBCert.mode,
+
+  /**
+   * Verify that listener settings are appropriate for this authentication mode.
+   *
+   * @param listener SocketListener
+   *        The socket listener about to be opened.
+   * @throws if validation requirements are not met
+   */
+  validateOptions(listener) {
+    if (!listener.encryption) {
+      throw new Error(OOBCert.mode + " authentication requires encryption.");
+    }
+  },
+
+  /**
+   * Augment options on the listening socket about to be opened.
+   *
+   * @param listener SocketListener
+   *        The socket listener about to be opened.
+   * @param socket nsIServerSocket
+   *        The socket that is about to start listening.
+   */
+  augmentSocketOptions(listener, socket) {
+    let requestCert = Ci.nsITLSServerSocket.REQUIRE_ALWAYS;
+    socket.setRequestClientCertificate(requestCert);
+  },
+
+  /**
+   * Augment the service discovery advertisement with any additional data needed
+   * to support this authentication mode.
+   *
+   * @param listener SocketListener
+   *        The socket listener that was just opened.
+   * @param advertisement object
+   *        The advertisement being built.
+   */
+  augmentAdvertisement(listener, advertisement) {
+    advertisement.authentication = OOBCert.mode;
+    // Step A.4
+    // Server announces itself via service discovery
+    // Announcement contains hash(ServerCert) as additional data
+    advertisement.cert = listener.cert;
+  },
+
+  /**
+   * Determine whether a connection the server should be allowed or not based on
+   * this authenticator's policies.
+   *
+   * @param session object
+   *        In OOB_CERT mode, the |session| includes:
+   *        {
+   *          client: {
+   *            host,
+   *            port,
+   *            cert: {
+   *              sha256
+   *            },
+   *          },
+   *          server: {
+   *            host,
+   *            port,
+   *            cert: {
+   *              sha256
+   *            }
+   *          },
+   *          transport
+   *        }
+   * @return An AuthenticationResult value.
+   *         A promise that will be resolved to the above is also allowed.
+   */
+  authenticate: Task.async(function*({ client, server, transport }) {
+    // Step B.3 / C.3
+    // TLS connection established, authentication begins
+    // TODO: Bug 1032128: Consult a list of persisted, approved clients
+    // Step B.4
+    // Server sees that ClientCert is from a unknown client
+    // Tell client they are unknown and should display OOB client UX
+    transport.send({
+      authResult: AuthenticationResult.PENDING
+    });
+
+    // Step B.5
+    // User is shown a Allow / Deny / Always Allow prompt on the Server
+    // with Client name and hash(ClientCert)
+    let result = yield this.allowConnection({
+      authentication: this.mode,
+      client,
+      server
+    });
+
+    switch (result) {
+      case AuthenticationResult.ALLOW_PERSIST:
+        // TODO: Bug 1032128: Persist the client
+      case AuthenticationResult.ALLOW:
+        break; // Further processing
+      default:
+        return result; // Abort for any negative results
+    }
+
+    // Examine additional data for authentication
+    let oob = yield this.receiveOOB();
+    if (!oob) {
+      dumpn("Invalid OOB data received");
+      return AuthenticationResult.DENY;
+    }
+
+    let { sha256, k } = oob;
+    // The OOB auth prompt should have transferred:
+    // hash(ClientCert) + K(random 128-bit number)
+    // from the client.
+    if (!sha256 || !k) {
+      dumpn("Invalid OOB data received");
+      return AuthenticationResult.DENY;
+    }
+
+    // Step B.10
+    // Server verifies that Client's cert matches hash(ClientCert) from
+    // out-of-band channel
+    if (client.cert.sha256 != sha256) {
+      dumpn("Client cert hash doesn't match OOB data");
+      return AuthenticationResult.DENY;
+    }
+
+    // Step B.11
+    // Server sends K to Client over TLS connection
+    transport.send({ authResult: result, k });
+
+    // Client may decide to abort if K does not match.
+    // Server's portion of authentication is now complete.
+
+    // Step B.13
+    // Debugging begins
+    return result;
+  }),
+
+  /**
+   * Prompt the user to accept or decline the incoming connection. The default
+   * implementation is used unless this is overridden on a particular
+   * authenticator instance.
+   *
+   * It is expected that the implementation of |allowConnection| will show a
+   * prompt to the user so that they can allow or deny the connection.
+   *
+   * @param session object
+   *        In OOB_CERT mode, the |session| includes:
+   *        {
+   *          client: {
+   *            host,
+   *            port,
+   *            cert: {
+   *              sha256
+   *            },
+   *          },
+   *          server: {
+   *            host,
+   *            port,
+   *            cert: {
+   *              sha256
+   *            }
+   *          }
+   *        }
+   * @return An AuthenticationResult value.
+   *         A promise that will be resolved to the above is also allowed.
+   */
+  allowConnection: prompt.Server.defaultAllowConnection,
+
+  /**
+   * The user must transfer some data through some out of band mechanism from
+   * the client to the server to authenticate the devices.
+   *
+   * @return An object containing:
+   *         * sha256: hash(ClientCert)
+   *         * k     : K(random 128-bit number)
+   *         A promise that will be resolved to the above is also allowed.
+   */
+  receiveOOB: prompt.Server.defaultReceiveOOB,
+
+};
+
+exports.Authenticators = {
+  get(mode) {
+    if (!mode) {
+      mode = Prompt.mode;
+    }
+    for (let key in Authenticators) {
+      let auth = Authenticators[key];
+      if (auth.mode === mode) {
+        return auth;
+      }
+    }
+    throw new Error("Unknown authenticator mode: " + mode);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/docs/wifi.md
@@ -0,0 +1,154 @@
+Overview
+--------
+
+### Remote Debugging Today
+
+Connecting to the Dev Tools debugging server on a remote device (like
+B2G) via USB (which requires ADB) is too complex to setup and use.
+Dealing with ADB is confusing, especially on Windows and Linux where
+there are driver issues / udev rules to set up first. We have made
+various attempts to simplify this and probably will continue to try our
+best, but it doesn't seem like the UX will ever be great with ADB
+involved.
+
+### Wi-Fi
+
+We're interested in making the debugging server available over Wi-Fi,
+mainly in an attempt to simplify the UX. This of course presents new
+security challenges to address, but we must also keep in mind that **if
+our plan to address security results in a complex UX, then it may not be
+a net gain over the USB & ADB route**.
+
+To be clear, we are not trying to expose ADB over Wi-Fi at all, only the
+Dev Tools debugging server.
+
+### Security
+
+TLS is used to provide encryption of the data in transit. Both parties
+use self-signed certs to identify themselves. There is a one-time setup
+process to authenticate a new device. This is explained in many more
+details later on in this document.
+
+Definitions
+-----------
+
+-   **Device / Server**: Firefox OS phone (or Fennec, remote Firefox,
+    etc.)
+-   **Computer / Client**: Machine running desktop Firefox w/ WebIDE
+
+Proposal
+--------
+
+This proposal uses TLS with self-signed certs to allow Clients to
+connect to Servers through an encrypted, authenticated channel. After the
+first connection from a new Client, the Client is saved on the Server
+(if the user wants to always allow) and can connect freely in the future
+(assuming Wi-Fi debugging is still enabled).
+
+### Default State
+
+The device does not listen over Wi-Fi at all by default.
+
+### Part A: Enabling Wi-Fi Debugging
+
+1.  User goes to Developer menu on Device
+2.  User checks "DevTools over Wi-Fi" to enable the feature
+    -   Persistent notification displayed in notification bar reminding
+        user that this is enabled
+
+3.  Device begins listening on random TCP socket via Wi-Fi only
+4.  Device announces itself via service discovery
+    -   Announcements only go to the local LAN / same subnet
+    -   The announcement contains hash(DeviceCert) as additional data
+
+The Device remains listening as long as the feature is enabled.
+
+### Part B: Using Wi-Fi Debugging (new computer)
+
+Here are the details of connecting from a new computer to the device:
+
+1.  Computer detects Device as available for connection via service
+    discovery
+2.  User chooses device to start connection on Computer
+3.  TLS connection established, authentication begins
+4.  Device sees that ComputerCert is from an unknown client (since it is
+    new)
+5.  User is shown an Allow / Deny / Always Allow prompt on the Device
+    with Computer name and hash(ComputerCert)
+    -   If Deny is chosen, the connection is terminated and exponential
+        backoff begins (larger with each successive Deny)
+    -   If Allow is chosen, the connection proceeds, but nothing is
+        stored for the future
+    -   If Always Allow is chosen, the connection proceeds, and
+        hash(ComputerCert) is saved for future attempts
+
+6.  Device waits for out-of-band data
+7.  Computer verifies that Device's cert matches hash(DeviceCert) from
+    the advertisement
+8.  Computer creates hash(ComputerCert) and K(random 128-bit number)
+9.  Out-of-band channel is used to move result of step 8 from Computer
+    to Device
+    -   For Firefox Desktop -\> Firefox OS, Desktop will make a QR code,
+        and FxOS will scan it
+    -   For non-mobile servers, some other approach is likely needed,
+        perhaps a short code form for the user to transfer
+
+10. Device verifies that Computer's cert matches hash(ComputerCert) from
+    out-of-band channel
+11. Device sends K to Computer over the TLS connection
+12. Computer verifies received value matches K
+13. Debugging begins
+
+### Part C: Using Wi-Fi Debugging (known computer)
+
+Here are the details of connecting from a known computer (saved via
+Always Allow) to the device:
+
+1.  Computer detects Device as available for connection via service
+    discovery
+2.  User choose device to start connection on Computer
+3.  TLS connection established, authentication begins
+4.  Device sees that ComputerCert is from a known client via
+    hash(ComputerCert)
+5.  Debugging begins
+
+### Other Details
+
+-   When there is a socket listening for connections, they will only be
+    accepted via Wi-Fi
+    -   The socket will only listen on the external, Wi-Fi interface
+    -   This is to ensure local apps can't connect to the socket
+-   Socket remains listening as long as the feature is enabled
+
+### UX
+
+This design seems convenient and of relatively low burden on the user.
+If they choose to save the Computer for future connections, it becomes a
+one click connection from Computer to Device, as it is over USB today.
+
+### Possible Attacks
+
+Someone could try to DoS the phone via many connection attempts. The
+exponential backoff should mitigate this concern. ([bug
+1022692](https://bugzilla.mozilla.org/show_bug.cgi?id=1022692))
+
+### Comparison to ADB
+
+While it would be nice if we could instead leverage ADB here, that
+doesn’t seem viable because:
+
+-   ADB comes with a lot of setup / troubleshooting pain
+    -   Users don’t understand it or why it is needed for us
+    -   Each OS has several UX issues with ADB that make it annoying to
+        use
+-   ADB has a much larger attack surface area, simply because it has
+    many more lower level functions than the Developer Tools protocol we
+    are exposing here
+
+Acknowledgments
+---------------
+
+-   J. Ryan Stinnett started this project from the DevTools team
+-   Brian Warner created many of the specific details of the authentication
+    protocol
+-   Trevor Perrin helped vet the authentication protocol
--- a/toolkit/devtools/security/moz.build
+++ b/toolkit/devtools/security/moz.build
@@ -16,11 +16,13 @@ UNIFIED_SOURCES += [
     'LocalCertService.cpp',
 ]
 
 FAIL_ON_WARNINGS = True
 
 FINAL_LIBRARY = 'xul'
 
 EXTRA_JS_MODULES.devtools.security += [
+    'auth.js',
     'cert.js',
+    'prompt.js',
     'socket.js',
 ]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/prompt.js
@@ -0,0 +1,172 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript 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/. */
+
+"use strict";
+
+let { Ci } = require("chrome");
+let Services = require("Services");
+let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
+loader.lazyRequireGetter(this, "DebuggerSocket",
+  "devtools/toolkit/security/socket", true);
+loader.lazyRequireGetter(this, "AuthenticationResult",
+  "devtools/toolkit/security/auth", true);
+
+DevToolsUtils.defineLazyGetter(this, "bundle", () => {
+  const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties";
+  return Services.strings.createBundle(DBG_STRINGS_URI);
+});
+
+let Client = exports.Client = {};
+let Server = exports.Server = {};
+
+/**
+ * During OOB_CERT authentication, a notification dialog like this is used to
+ * to display a token which the user must transfer through some mechanism to the
+ * server to authenticate the devices.
+ *
+ * This implementation presents the token as text for the user to transfer
+ * manually.  For a mobile device, you should override this implementation with
+ * something more convenient, such as displaying a QR code.
+ *
+ * @param host string
+ *        The host name or IP address of the debugger server.
+ * @param port number
+ *        The port number of the debugger server.
+ * @param cert object (optional)
+ *        The server's cert details.
+ * @param authResult AuthenticationResult
+ *        Authentication result sent from the server.
+ * @param oob object (optional)
+ *        The token data to be transferred during OOB_CERT step 8:
+ *        * sha256: hash(ClientCert)
+ *        * k     : K(random 128-bit number)
+ * @return object containing:
+ *         * close: Function to hide the notification
+ */
+Client.defaultSendOOB = ({ authResult, oob }) => {
+  // Only show in the PENDING state
+  if (authResult != AuthenticationResult.PENDING) {
+    throw new Error("Expected PENDING result, got " + authResult);
+  }
+  let title = bundle.GetStringFromName("clientSendOOBTitle");
+  let header = bundle.GetStringFromName("clientSendOOBHeader");
+  let hashMsg = bundle.formatStringFromName("clientSendOOBHash",
+                                            [oob.sha256], 1);
+  let token = oob.sha256.replace(/:/g, "").toLowerCase() + oob.k;
+  let tokenMsg = bundle.formatStringFromName("clientSendOOBToken",
+                                             [token], 1);
+  let msg =`${header}\n\n${hashMsg}\n${tokenMsg}`;
+  let prompt = Services.prompt;
+  let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_CANCEL;
+
+  // Listen for the window our prompt opens, so we can close it programatically
+  let promptWindow;
+  let windowListener = {
+    onOpenWindow(xulWindow) {
+      let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindow);
+      win.addEventListener("load", function listener() {
+        win.removeEventListener("load", listener, false);
+        if (win.document.documentElement.getAttribute("id") != "commonDialog") {
+          return;
+        }
+        // Found the window
+        promptWindow = win;
+        Services.wm.removeListener(windowListener);
+      }, false);
+    },
+    onCloseWindow() {},
+    onWindowTitleChange() {}
+  };
+  Services.wm.addListener(windowListener);
+
+  // nsIPrompt is typically a blocking API, so |executeSoon| to get around this
+  DevToolsUtils.executeSoon(() => {
+    prompt.confirmEx(null, title, msg, flags, null, null, null, null,
+                     { value: false });
+  });
+
+  return {
+    close() {
+      if (!promptWindow) {
+        return;
+      }
+      promptWindow.document.documentElement.acceptDialog();
+      promptWindow = null;
+    }
+  };
+};
+
+/**
+ * Prompt the user to accept or decline the incoming connection. This is the
+ * default implementation that products embedding the debugger server may
+ * choose to override.  This can be overridden via |allowConnection| on the
+ * socket's authenticator instance.
+ *
+ * @return An AuthenticationResult value.
+ *         A promise that will be resolved to the above is also allowed.
+ */
+Server.defaultAllowConnection = ({ client, server }) => {
+  let title = bundle.GetStringFromName("remoteIncomingPromptTitle");
+  let header = bundle.GetStringFromName("remoteIncomingPromptHeader");
+  let clientEndpoint = `${client.host}:${client.port}`;
+  let clientMsg =
+    bundle.formatStringFromName("remoteIncomingPromptClientEndpoint",
+                                [clientEndpoint], 1);
+  let serverEndpoint = `${server.host}:${server.port}`;
+  let serverMsg =
+    bundle.formatStringFromName("remoteIncomingPromptServerEndpoint",
+                                [serverEndpoint], 1);
+  let footer = bundle.GetStringFromName("remoteIncomingPromptFooter");
+  let msg =`${header}\n\n${clientMsg}\n${serverMsg}\n\n${footer}`;
+  let disableButton = bundle.GetStringFromName("remoteIncomingPromptDisable");
+  let prompt = Services.prompt;
+  let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_OK +
+              prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_CANCEL +
+              prompt.BUTTON_POS_2 * prompt.BUTTON_TITLE_IS_STRING +
+              prompt.BUTTON_POS_1_DEFAULT;
+  let result = prompt.confirmEx(null, title, msg, flags, null, null,
+                                disableButton, null, { value: false });
+  if (result === 0) {
+    return AuthenticationResult.ALLOW;
+  }
+  if (result === 2) {
+    return AuthenticationResult.DISABLE_ALL;
+  }
+  return AuthenticationResult.DENY;
+};
+
+/**
+ * During OOB_CERT authentication, the user must transfer some data through some
+ * out of band mechanism from the client to the server to authenticate the
+ * devices.
+ *
+ * This implementation prompts the user for a token as constructed by
+ * |Client.defaultSendOOB| that the user needs to transfer manually.  For a
+ * mobile device, you should override this implementation with something more
+ * convenient, such as reading a QR code.
+ *
+ * @return An object containing:
+ *         * sha256: hash(ClientCert)
+ *         * k     : K(random 128-bit number)
+ *         A promise that will be resolved to the above is also allowed.
+ */
+Server.defaultReceiveOOB = () => {
+  let title = bundle.GetStringFromName("serverReceiveOOBTitle");
+  let msg = bundle.GetStringFromName("serverReceiveOOBBody");
+  let input = { value: null };
+  let prompt = Services.prompt;
+  let result = prompt.prompt(null, title, msg, input, null, { value: false });
+  if (!result) {
+    return null;
+  }
+  // Re-create original object from token
+  input = input.value.trim();
+  let sha256 = input.substring(0, 64);
+  sha256 = sha256.replace(/\w{2}/g, "$&:").slice(0, -1).toUpperCase();
+  let k = input.substring(64);
+  return { sha256, k };
+};
--- a/toolkit/devtools/security/socket.js
+++ b/toolkit/devtools/security/socket.js
@@ -1,33 +1,37 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript 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/. */
 
 "use strict";
 
-let { Ci, Cc, CC, Cr } = require("chrome");
+let { Ci, Cc, CC, Cr, Cu } = require("chrome");
 
 // Ensure PSM is initialized to support TLS sockets
 Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
 
 let Services = require("Services");
 let promise = require("promise");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 let { dumpn, dumpv } = DevToolsUtils;
 loader.lazyRequireGetter(this, "DebuggerTransport",
   "devtools/toolkit/transport/transport", true);
 loader.lazyRequireGetter(this, "DebuggerServer",
   "devtools/server/main", true);
 loader.lazyRequireGetter(this, "discovery",
   "devtools/toolkit/discovery/discovery");
 loader.lazyRequireGetter(this, "cert",
   "devtools/toolkit/security/cert");
+loader.lazyRequireGetter(this, "Authenticators",
+  "devtools/toolkit/security/auth", true);
+loader.lazyRequireGetter(this, "AuthenticationResult",
+  "devtools/toolkit/security/auth", true);
 loader.lazyRequireGetter(this, "setTimeout", "Timer", true);
 loader.lazyRequireGetter(this, "clearTimeout", "Timer", true);
 
 DevToolsUtils.defineLazyGetter(this, "nsFile", () => {
   return CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath");
 });
 
 DevToolsUtils.defineLazyGetter(this, "socketTransportService", () => {
@@ -43,75 +47,155 @@ DevToolsUtils.defineLazyGetter(this, "ce
 DevToolsUtils.defineLazyGetter(this, "nssErrorsService", () => {
   return Cc["@mozilla.org/nss_errors_service;1"]
          .getService(Ci.nsINSSErrorsService);
 });
 
 DevToolsUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 
-const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties";
-
 let DebuggerSocket = {};
 
 /**
  * Connects to a debugger server socket.
  *
  * @param host string
  *        The host name or IP address of the debugger server.
  * @param port number
  *        The port number of the debugger server.
  * @param encryption boolean (optional)
  *        Whether the server requires encryption.  Defaults to false.
+ * @param authenticator Authenticator (optional)
+ *        |Authenticator| instance matching the mode in use by the server.
+ *        Defaults to a PROMPT instance if not supplied.
+ * @param cert object (optional)
+ *        The server's cert details.  Used with OOB_CERT authentication.
  * @return promise
  *         Resolved to a DebuggerTransport instance.
  */
-DebuggerSocket.connect = Task.async(function*({ host, port, encryption }) {
-  let attempt = yield _attemptTransport({ host, port, encryption });
+DebuggerSocket.connect = Task.async(function*(settings) {
+  // Default to PROMPT |Authenticator| instance if not supplied
+  if (!settings.authenticator) {
+    settings.authenticator = new (Authenticators.get().Client)();
+  }
+  let { host, port, encryption, authenticator, cert } = settings;
+  let transport = yield _getTransport(settings);
+  yield authenticator.authenticate({
+    host,
+    port,
+    encryption,
+    cert,
+    transport
+  });
+  return transport;
+});
+
+/**
+ * Try very hard to create a DevTools transport, potentially making several
+ * connect attempts in the process.
+ *
+ * @param host string
+ *        The host name or IP address of the debugger server.
+ * @param port number
+ *        The port number of the debugger server.
+ * @param encryption boolean (optional)
+ *        Whether the server requires encryption.  Defaults to false.
+ * @param authenticator Authenticator
+ *        |Authenticator| instance matching the mode in use by the server.
+ *        Defaults to a PROMPT instance if not supplied.
+ * @param cert object (optional)
+ *        The server's cert details.  Used with OOB_CERT authentication.
+ * @return transport DebuggerTransport
+ *         A possible DevTools transport (if connection succeeded and streams
+ *         are actually alive and working)
+ * @return certError boolean
+ *         Flag noting if cert trouble caused the streams to fail
+ * @return s nsISocketTransport
+ *         Underlying socket transport, in case more details are needed.
+ */
+let _getTransport = Task.async(function*(settings) {
+  let { host, port, encryption } = settings;
+  let attempt = yield _attemptTransport(settings);
   if (attempt.transport) {
     return attempt.transport; // Success
   }
 
   // If the server cert failed validation, store a temporary override and make
   // a second attempt.
   if (encryption && attempt.certError) {
     _storeCertOverride(attempt.s, host, port);
   } else {
     throw new Error("Connection failed");
   }
 
-  attempt = yield _attemptTransport({ host, port, encryption });
+  attempt = yield _attemptTransport(settings);
   if (attempt.transport) {
     return attempt.transport; // Success
   }
 
   throw new Error("Connection failed even after cert override");
 });
 
 /**
- * Try to connect and create a DevTools transport.
+ * Make a single attempt to connect and create a DevTools transport.  This could
+ * fail if the remote host is unreachable, for example.  If there is security
+ * error due to the use of self-signed certs, you should make another attempt
+ * after storing a cert override.
  *
+ * @param host string
+ *        The host name or IP address of the debugger server.
+ * @param port number
+ *        The port number of the debugger server.
+ * @param encryption boolean (optional)
+ *        Whether the server requires encryption.  Defaults to false.
+ * @param authenticator Authenticator
+ *        |Authenticator| instance matching the mode in use by the server.
+ *        Defaults to a PROMPT instance if not supplied.
+ * @param cert object (optional)
+ *        The server's cert details.  Used with OOB_CERT authentication.
  * @return transport DebuggerTransport
  *         A possible DevTools transport (if connection succeeded and streams
  *         are actually alive and working)
  * @return certError boolean
  *         Flag noting if cert trouble caused the streams to fail
  * @return s nsISocketTransport
  *         Underlying socket transport, in case more details are needed.
  */
-let _attemptTransport = Task.async(function*({ host, port, encryption }){
+let _attemptTransport = Task.async(function*(settings) {
+  let { authenticator } = settings;
   // _attemptConnect only opens the streams.  Any failures at that stage
   // aborts the connection process immedidately.
-  let { s, input, output } = _attemptConnect({ host, port, encryption });
+  let { s, input, output } = yield _attemptConnect(settings);
 
   // Check if the input stream is alive.  If encryption is enabled, we need to
   // watch out for cert errors by testing the input stream.
-  let { alive, certError } = yield _isInputAlive(input);
+  let alive, certError;
+  try {
+    let results = yield _isInputAlive(input);
+    alive = results.alive;
+    certError = results.certError;
+  } catch(e) {
+    // For other unexpected errors, like NS_ERROR_CONNECTION_REFUSED, we reach
+    // this block.
+    input.close();
+    output.close();
+    throw e;
+  }
   dumpv("Server cert accepted? " + !certError);
 
+  // The |Authenticator| examines the connection as well and may determine it
+  // should be dropped.
+  alive = alive && authenticator.validateConnection({
+    host: settings.host,
+    port: settings.port,
+    encryption: settings.encryption,
+    cert: settings.cert,
+    socket: s
+  });
+
   let transport;
   if (alive) {
     transport = new DebuggerTransport(input, output);
   } else {
     // Something went wrong, close the streams.
     input.close();
     output.close();
   }
@@ -128,43 +212,85 @@ let _attemptTransport = Task.async(funct
  * fail later when the streams are actually used.
  * @return s nsISocketTransport
  *         Underlying socket transport, in case more details are needed.
  * @return input nsIAsyncInputStream
  *         The socket's input stream.
  * @return output nsIAsyncOutputStream
  *         The socket's output stream.
  */
-function _attemptConnect({ host, port, encryption }) {
+let _attemptConnect = Task.async(function*({ host, port, encryption }) {
   let s;
   if (encryption) {
     s = socketTransportService.createTransport(["ssl"], 1, host, port, null);
   } else {
     s = socketTransportService.createTransport(null, 0, host, port, null);
   }
   // By default the CONNECT socket timeout is very long, 65535 seconds,
   // so that if we race to be in CONNECT state while the server socket is still
   // initializing, the connection is stuck in connecting state for 18.20 hours!
   s.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2);
 
+  // If encrypting, load the client cert now, so we can deliver it at just the
+  // right time.
+  let clientCert;
+  if (encryption) {
+    clientCert = yield cert.local.getOrCreate();
+  }
+
+  let deferred = promise.defer();
+  let input;
+  let output;
+  // Delay opening the input stream until the transport has fully connected.
+  // The goal is to avoid showing the user a client cert UI prompt when
+  // encryption is used.  This prompt is shown when the client opens the input
+  // stream and does not know which client cert to present to the server.  To
+  // specify a client cert programmatically, we need to access the transport's
+  // nsISSLSocketControl interface, which is not accessible until the transport
+  // has connected.
+  s.setEventSink({
+    onTransportStatus(transport, status) {
+      if (status != Ci.nsISocketTransport.STATUS_CONNECTING_TO) {
+        return;
+      }
+      if (encryption) {
+        let sslSocketControl =
+          transport.securityInfo.QueryInterface(Ci.nsISSLSocketControl);
+        sslSocketControl.clientCert = clientCert;
+      }
+      try {
+        input = s.openInputStream(0, 0, 0);
+      } catch(e) {
+        deferred.reject(e);
+      }
+      deferred.resolve({ s, input, output });
+    }
+  }, Services.tm.currentThread);
+
   // openOutputStream may throw NS_ERROR_NOT_INITIALIZED if we hit some race
   // where the nsISocketTransport gets shutdown in between its instantiation and
   // the call to this method.
-  let input;
-  let output;
   try {
-    input = s.openInputStream(0, 0, 0);
     output = s.openOutputStream(0, 0, 0);
   } catch(e) {
-    DevToolsUtils.reportException("_attemptConnect", e);
-    throw e;
+    deferred.reject(e);
   }
 
-  return { s, input, output };
-}
+  deferred.promise.catch(e => {
+    if (input) {
+      input.close();
+    }
+    if (output) {
+      output.close();
+    }
+    DevToolsUtils.reportException("_attemptConnect", e);
+  });
+
+  return deferred.promise;
+});
 
 /**
  * Check if the input stream is alive.  For an encrypted connection, it may not
  * be if the client refuses the server's cert.  A cert error is expected on
  * first connection to a new host because the cert is self-signed.
  */
 function _isInputAlive(input) {
   let deferred = promise.defer();
@@ -207,88 +333,59 @@ function _storeCertOverride(s, host, por
 
 /**
  * Creates a new socket listener for remote connections to the DebuggerServer.
  * This helps contain and organize the parts of the server that may differ or
  * are particular to one given listener mechanism vs. another.
  */
 function SocketListener() {}
 
-/**
- * Prompt the user to accept or decline the incoming connection. This is the
- * default implementation that products embedding the debugger server may
- * choose to override.  A separate security handler can be specified for each
- * socket via |allowConnection| on a socket listener instance.
- *
- * @return true if the connection should be permitted, false otherwise
- */
-SocketListener.defaultAllowConnection = () => {
-  let bundle = Services.strings.createBundle(DBG_STRINGS_URI);
-  let title = bundle.GetStringFromName("remoteIncomingPromptTitle");
-  let msg = bundle.GetStringFromName("remoteIncomingPromptMessage");
-  let disableButton = bundle.GetStringFromName("remoteIncomingPromptDisable");
-  let prompt = Services.prompt;
-  let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_OK +
-              prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_CANCEL +
-              prompt.BUTTON_POS_2 * prompt.BUTTON_TITLE_IS_STRING +
-              prompt.BUTTON_POS_1_DEFAULT;
-  let result = prompt.confirmEx(null, title, msg, flags, null, null,
-                                disableButton, null, { value: false });
-  if (result === 0) {
-    return true;
-  }
-  if (result === 2) {
-    DebuggerServer.closeAllListeners();
-    Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
-  }
-  return false;
-};
-
 SocketListener.prototype = {
 
   /* Socket Options */
 
   /**
    * The port or path to listen on.
    *
    * If given an integer, the port to listen on.  Use -1 to choose any available
    * port. Otherwise, the path to the unix socket domain file to listen on.
    */
   portOrPath: null,
 
   /**
-   * Prompt the user to accept or decline the incoming connection. The default
-   * implementation is used unless this is overridden on a particular socket
-   * listener instance.
-   *
-   * @return true if the connection should be permitted, false otherwise
-   */
-  allowConnection: SocketListener.defaultAllowConnection,
-
-  /**
    * Controls whether this listener is announced via the service discovery
    * mechanism.
    */
   discoverable: false,
 
   /**
    * Controls whether this listener's transport uses encryption.
    */
   encryption: false,
 
   /**
+   * Controls the |Authenticator| used, which hooks various socket steps to
+   * implement an authentication policy.  It is expected that different use
+   * cases may override pieces of the |Authenticator|.  See auth.js.
+   *
+   * Here we set the default |Authenticator|, which is |Prompt|.
+   */
+  authenticator: new (Authenticators.get().Server)(),
+
+  /**
    * Validate that all options have been set to a supported configuration.
    */
   _validateOptions: function() {
     if (this.portOrPath === null) {
       throw new Error("Must set a port / path to listen on.");
     }
     if (this.discoverable && !Number(this.portOrPath)) {
       throw new Error("Discovery only supported for TCP sockets.");
     }
+    this.authenticator.validateOptions(this);
   },
 
   /**
    * Listens on the given port or socket file for remote debugger connections.
    */
   open: function() {
     this._validateOptions();
     DebuggerServer._addListener(this);
@@ -312,29 +409,39 @@ SocketListener.prototype = {
           file.remove(false);
         }
         self._socket.initWithFilename(file, parseInt("666", 8), backlog);
       }
       yield self._setAdditionalSocketOptions();
       self._socket.asyncListen(self);
       dumpn("Socket listening on: " + (self.port || self.portOrPath));
     }).then(() => {
-      if (this.discoverable && this.port) {
-        discovery.addService("devtools", {
-          port: this.port,
-          encryption: this.encryption
-        });
-      }
+      this._advertise();
     }).catch(e => {
       dumpn("Could not start debugging listener on '" + this.portOrPath +
             "': " + e);
       this.close();
     });
   },
 
+  _advertise: function() {
+    if (!this.discoverable || !this.port) {
+      return;
+    }
+
+    let advertisement = {
+      port: this.port,
+      encryption: this.encryption,
+    };
+
+    this.authenticator.augmentAdvertisement(this, advertisement);
+
+    discovery.addService("devtools", advertisement);
+  },
+
   _createSocketInstance: function() {
     if (this.encryption) {
       return Cc["@mozilla.org/network/tls-server-socket;1"]
              .createInstance(Ci.nsITLSServerSocket);
     }
     return Cc["@mozilla.org/network/server-socket;1"]
            .createInstance(Ci.nsIServerSocket);
   },
@@ -342,16 +449,17 @@ SocketListener.prototype = {
   _setAdditionalSocketOptions: Task.async(function*() {
     if (this.encryption) {
       this._socket.serverCert = yield cert.local.getOrCreate();
       this._socket.setSessionCache(false);
       this._socket.setSessionTickets(false);
       let requestCert = Ci.nsITLSServerSocket.REQUEST_NEVER;
       this._socket.setRequestClientCertificate(requestCert);
     }
+    this.authenticator.augmentSocketOptions(this, this._socket);
   }),
 
   /**
    * Closes the SocketListener.  Notifies the server to remove the listener from
    * the set of active SocketListeners.
    */
   close: function() {
     if (this.discoverable && this.port) {
@@ -359,16 +467,26 @@ SocketListener.prototype = {
     }
     if (this._socket) {
       this._socket.close();
       this._socket = null;
     }
     DebuggerServer._removeListener(this);
   },
 
+  get host() {
+    if (!this._socket) {
+      return null;
+    }
+    if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
+      return "127.0.0.1";
+    }
+    return "0.0.0.0";
+  },
+
   /**
    * Gets whether this listener uses a port number vs. a path.
    */
   get isPortBased() {
     return !!Number(this.portOrPath);
   },
 
   /**
@@ -377,93 +495,251 @@ SocketListener.prototype = {
    */
   get port() {
     if (!this.isPortBased || !this._socket) {
       return null;
     }
     return this._socket.port;
   },
 
+  get cert() {
+    if (!this._socket || !this._socket.serverCert) {
+      return null;
+    }
+    return {
+      sha256: this._socket.serverCert.sha256Fingerprint
+    };
+  },
+
   // nsIServerSocketListener implementation
 
   onSocketAccepted:
   DevToolsUtils.makeInfallible(function(socket, socketTransport) {
-    if (this.encryption) {
-      new SecurityObserver(socketTransport);
-    }
-    if (Services.prefs.getBoolPref("devtools.debugger.prompt-connection") &&
-        !this.allowConnection()) {
-      return;
-    }
-    dumpn("New debugging connection on " +
-          socketTransport.host + ":" + socketTransport.port);
-
-    let input = socketTransport.openInputStream(0, 0, 0);
-    let output = socketTransport.openOutputStream(0, 0, 0);
-    let transport = new DebuggerTransport(input, output);
-    DebuggerServer._onConnection(transport);
+    new ServerSocketConnection(this, socketTransport);
   }, "SocketListener.onSocketAccepted"),
 
   onStopListening: function(socket, status) {
     dumpn("onStopListening, status: " + status);
   }
 
 };
 
 // Client must complete TLS handshake within this window (ms)
 loader.lazyGetter(this, "HANDSHAKE_TIMEOUT", () => {
   return Services.prefs.getIntPref("devtools.remote.tls-handshake-timeout");
 });
 
-function SecurityObserver(socketTransport) {
-  this.socketTransport = socketTransport;
-  let connectionInfo = socketTransport.securityInfo
-                       .QueryInterface(Ci.nsITLSServerConnectionInfo);
-  connectionInfo.setSecurityObserver(this);
-  this._handshakeTimeout = setTimeout(this._onHandshakeTimeout.bind(this),
-                                      HANDSHAKE_TIMEOUT);
+/**
+ * A |ServerSocketConnection| is created by a |SocketListener| for each accepted
+ * incoming socket.  This is a short-lived object used to implement
+ * authentication and verify encryption prior to handing off the connection to
+ * the |DebuggerServer|.
+ */
+function ServerSocketConnection(listener, socketTransport) {
+  this._listener = listener;
+  this._socketTransport = socketTransport;
+  this._handle();
 }
 
-SecurityObserver.prototype = {
+ServerSocketConnection.prototype = {
+
+  get authentication() {
+    return this._listener.authenticator.mode;
+  },
+
+  get host() {
+    return this._socketTransport.host;
+  },
+
+  get port() {
+    return this._socketTransport.port;
+  },
+
+  get cert() {
+    if (!this._clientCert) {
+      return null;
+    }
+    return {
+      sha256: this._clientCert.sha256Fingerprint
+    };
+  },
+
+  get address() {
+    return this.host + ":" + this.port;
+  },
+
+  get client() {
+    let client = {
+      host: this.host,
+      port: this.port
+    };
+    if (this.cert) {
+      client.cert = this.cert;
+    }
+    return client;
+  },
+
+  get server() {
+    let server = {
+      host: this._listener.host,
+      port: this._listener.port
+    };
+    if (this._listener.cert) {
+      server.cert = this._listener.cert;
+    }
+    return server;
+  },
+
+  /**
+   * This is the main authentication workflow.  If any pieces reject a promise,
+   * the connection is denied.  If the entire process resolves successfully,
+   * the connection is finally handed off to the |DebuggerServer|.
+   */
+  _handle() {
+    dumpn("Debugging connection starting authentication on " + this.address);
+    let self = this;
+    Task.spawn(function*() {
+      self._listenForTLSHandshake();
+      self._createTransport();
+      yield self._awaitTLSHandshake();
+      yield self._authenticate();
+    }).then(() => this.allow()).catch(e => this.deny(e));
+  },
+
+  /**
+   * We need to open the streams early on, as that is required in the case of
+   * TLS sockets to keep the handshake moving.
+   */
+  _createTransport() {
+    let input = this._socketTransport.openInputStream(0, 0, 0);
+    let output = this._socketTransport.openOutputStream(0, 0, 0);
+    this._transport = new DebuggerTransport(input, output);
+    // Start up the transport to observe the streams in case they are closed
+    // early.  This allows us to clean up our state as well.
+    this._transport.hooks = {
+      onClosed: reason => {
+        this.deny(reason);
+      }
+    };
+    this._transport.ready();
+  },
+
+  /**
+   * Set the socket's security observer, which receives an event via the
+   * |onHandshakeDone| callback when the TLS handshake completes.
+   */
+  _setSecurityObserver(observer) {
+    if (!this._socketTransport || !this._socketTransport.securityInfo) {
+      return;
+    }
+    let connectionInfo = this._socketTransport.securityInfo
+                         .QueryInterface(Ci.nsITLSServerConnectionInfo);
+    connectionInfo.setSecurityObserver(observer);
+  },
+
+  /**
+   * When encryption is used, we wait for the client to complete the TLS
+   * handshake before proceeding.  The handshake details are validated in
+   * |onHandshakeDone|.
+   */
+  _listenForTLSHandshake() {
+    this._handshakeDeferred = promise.defer();
+    if (!this._listener.encryption) {
+      this._handshakeDeferred.resolve();
+      return;
+    }
+    this._setSecurityObserver(this);
+    this._handshakeTimeout = setTimeout(this._onHandshakeTimeout.bind(this),
+                                        HANDSHAKE_TIMEOUT);
+  },
+
+  _awaitTLSHandshake() {
+    return this._handshakeDeferred.promise;
+  },
 
   _onHandshakeTimeout() {
-    dumpv("Client failed to complete handshake");
-    this.destroy(Cr.NS_ERROR_NET_TIMEOUT);
+    dumpv("Client failed to complete TLS handshake");
+    this._handshakeDeferred.reject(Cr.NS_ERROR_NET_TIMEOUT);
   },
 
   // nsITLSServerSecurityObserver implementation
   onHandshakeDone(socket, clientStatus) {
     clearTimeout(this._handshakeTimeout);
+    this._setSecurityObserver(null);
     dumpv("TLS version:    " + clientStatus.tlsVersionUsed.toString(16));
     dumpv("TLS cipher:     " + clientStatus.cipherName);
     dumpv("TLS key length: " + clientStatus.keyLength);
     dumpv("TLS MAC length: " + clientStatus.macLength);
+    this._clientCert = clientStatus.peerCert;
     /*
      * TODO: These rules should be really be set on the TLS socket directly, but
      * this would need more platform work to expose it via XPCOM.
      *
-     * Server *will* send hello packet when any rules below are not met, but the
-     * socket then closes after that.
-     *
      * Enforcing cipher suites here would be a bad idea, as we want TLS
      * cipher negotiation to work correctly.  The server already allows only
      * Gecko's normal set of cipher suites.
      */
     if (clientStatus.tlsVersionUsed != Ci.nsITLSClientStatus.TLS_VERSION_1_2) {
-      this.destroy(Cr.NS_ERROR_CONNECTION_REFUSED);
+      this._handshakeDeferred.reject(Cr.NS_ERROR_CONNECTION_REFUSED);
+      return;
     }
+
+    this._handshakeDeferred.resolve();
   },
 
-  destroy(result) {
+  _authenticate: Task.async(function*() {
+    let result = yield this._listener.authenticator.authenticate({
+      client: this.client,
+      server: this.server,
+      transport: this._transport
+    });
+    switch (result) {
+      case AuthenticationResult.DISABLE_ALL:
+        DebuggerServer.closeAllListeners();
+        Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
+        return promise.reject(Cr.NS_ERROR_CONNECTION_REFUSED);
+      case AuthenticationResult.DENY:
+        return promise.reject(Cr.NS_ERROR_CONNECTION_REFUSED);
+      case AuthenticationResult.ALLOW:
+      case AuthenticationResult.ALLOW_PERSIST:
+        return promise.resolve();
+      default:
+        return promise.reject(Cr.NS_ERROR_CONNECTION_REFUSED);
+    }
+  }),
+
+  deny(result) {
+    let errorName = result;
+    for (let name in Cr) {
+      if (Cr[name] === result) {
+        errorName = name;
+        break;
+      }
+    }
+    dumpn("Debugging connection denied on " + this.address +
+          " (" + errorName + ")");
+    this._transport.hooks = null;
+    this._transport.close(result);
+    this._socketTransport.close(result);
+    this.destroy();
+  },
+
+  allow() {
+    dumpn("Debugging connection allowed on " + this.address);
+    DebuggerServer._onConnection(this._transport);
+    this.destroy();
+  },
+
+  destroy() {
     clearTimeout(this._handshakeTimeout);
-    let connectionInfo = this.socketTransport.securityInfo
-                         .QueryInterface(Ci.nsITLSServerConnectionInfo);
-    connectionInfo.setSecurityObserver(null);
-    this.socketTransport.close(result);
-    this.socketTransport = null;
+    this._setSecurityObserver(null);
+    this._listener = null;
+    this._socketTransport = null;
+    this._transport = null;
+    this._clientCert = null;
   }
 
 };
 
 DebuggerSocket.createListener = function() {
   return new SocketListener();
 };
 
--- a/toolkit/devtools/security/tests/unit/test_encryption.js
+++ b/toolkit/devtools/security/tests/unit/test_encryption.js
@@ -23,20 +23,26 @@ function connectClient(client) {
 add_task(function*() {
   initTestDebuggerServer();
 });
 
 // Client w/ encryption connects successfully to server w/ encryption
 add_task(function*() {
   equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
 
+  let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
+  let authenticator = new AuthenticatorType.Server();
+  authenticator.allowConnection = () => {
+    return DebuggerServer.AuthenticationResult.ALLOW;
+  };
+
   let listener = DebuggerServer.createListener();
   ok(listener, "Socket listener created");
   listener.portOrPath = -1 /* any available port */;
-  listener.allowConnection = () => true;
+  listener.authenticator = authenticator;
   listener.encryption = true;
   yield listener.open();
   equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
 
   let transport = yield DebuggerClient.socketConnect({
     host: "127.0.0.1",
     port: listener.port,
     encryption: true
@@ -64,20 +70,26 @@ add_task(function*() {
   listener.close();
   equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
 });
 
 // Client w/o encryption fails to connect to server w/ encryption
 add_task(function*() {
   equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
 
+  let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
+  let authenticator = new AuthenticatorType.Server();
+  authenticator.allowConnection = () => {
+    return DebuggerServer.AuthenticationResult.ALLOW;
+  };
+
   let listener = DebuggerServer.createListener();
   ok(listener, "Socket listener created");
   listener.portOrPath = -1 /* any available port */;
-  listener.allowConnection = () => true;
+  listener.authenticator = authenticator;
   listener.encryption = true;
   yield listener.open();
   equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
 
   try {
     yield DebuggerClient.socketConnect({
       host: "127.0.0.1",
       port: listener.port
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/tests/unit/test_oob_cert_auth.js
@@ -0,0 +1,264 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+devtools.lazyRequireGetter(this, "cert",
+  "devtools/toolkit/security/cert");
+
+// Test basic functionality of DevTools client and server OOB_CERT auth (used
+// with WiFi debugging)
+function run_test() {
+  // Need profile dir to store the key / cert
+  do_get_profile();
+  // Ensure PSM is initialized
+  Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+  run_next_test();
+}
+
+function connectClient(client) {
+  let deferred = promise.defer();
+  client.connect(() => {
+    client.listTabs(deferred.resolve);
+  });
+  return deferred.promise;
+}
+
+add_task(function*() {
+  initTestDebuggerServer();
+});
+
+// Client w/ OOB_CERT auth connects successfully to server w/ OOB_CERT auth
+add_task(function*() {
+  equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+  // Grab our cert, instead of relying on a discovery advertisement
+  let serverCert = yield cert.local.getOrCreate();
+
+  let oobData = promise.defer();
+  let AuthenticatorType = DebuggerServer.Authenticators.get("OOB_CERT");
+  let serverAuth = new AuthenticatorType.Server();
+  serverAuth.allowConnection = () => {
+    return DebuggerServer.AuthenticationResult.ALLOW;
+  };
+  serverAuth.receiveOOB = () => oobData.promise; // Skip prompt for tests
+
+  let listener = DebuggerServer.createListener();
+  ok(listener, "Socket listener created");
+  listener.portOrPath = -1 /* any available port */;
+  listener.encryption = true /* required for OOB_CERT */;
+  listener.authenticator = serverAuth;
+  yield listener.open();
+  equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+  let clientAuth = new AuthenticatorType.Client();
+  clientAuth.sendOOB = ({ oob }) => {
+    do_print(oob);
+    // Pass to server, skipping prompt for tests
+    oobData.resolve(oob);
+  };
+
+  let transport = yield DebuggerClient.socketConnect({
+    host: "127.0.0.1",
+    port: listener.port,
+    encryption: true,
+    authenticator: clientAuth,
+    cert: {
+      sha256: serverCert.sha256Fingerprint
+    }
+  });
+  ok(transport, "Client transport created");
+
+  let client = new DebuggerClient(transport);
+  let onUnexpectedClose = () => {
+    do_throw("Closed unexpectedly");
+  };
+  client.addListener("closed", onUnexpectedClose);
+  yield connectClient(client);
+
+  // Send a message the server will echo back
+  let message = "secrets";
+  let reply = yield client.request({
+    to: "root",
+    type: "echo",
+    message
+  });
+  equal(reply.message, message, "Encrypted echo matches");
+
+  client.removeListener("closed", onUnexpectedClose);
+  transport.close();
+  listener.close();
+  equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+});
+
+// Client w/o OOB_CERT auth fails to connect to server w/ OOB_CERT auth
+add_task(function*() {
+  equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+  let oobData = promise.defer();
+  let AuthenticatorType = DebuggerServer.Authenticators.get("OOB_CERT");
+  let serverAuth = new AuthenticatorType.Server();
+  serverAuth.allowConnection = () => {
+    return DebuggerServer.AuthenticationResult.ALLOW;
+  };
+  serverAuth.receiveOOB = () => oobData.promise; // Skip prompt for tests
+
+  let listener = DebuggerServer.createListener();
+  ok(listener, "Socket listener created");
+  listener.portOrPath = -1 /* any available port */;
+  listener.encryption = true /* required for OOB_CERT */;
+  listener.authenticator = serverAuth;
+  yield listener.open();
+  equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+  // This will succeed, but leaves the client in confused state, and no data is
+  // actually accessible
+  let transport = yield DebuggerClient.socketConnect({
+    host: "127.0.0.1",
+    port: listener.port,
+    encryption: true
+    // authenticator: PROMPT is the default
+  });
+
+  // Attempt to use the transport
+  let deferred = promise.defer();
+  let client = new DebuggerClient(transport);
+  client.onPacket = packet => {
+    // Client did not authenticate, so it ends up seeing the server's auth data
+    // which is effectively malformed data from the client's perspective
+    ok(!packet.from && packet.authResult, "Got auth packet instead of data");
+    deferred.resolve();
+  };
+  client.connect();
+  yield deferred.promise;
+
+  // Try to send a message the server will echo back
+  let message = "secrets";
+  try {
+    yield client.request({
+      to: "root",
+      type: "echo",
+      message
+    });
+  } catch(e) {
+    ok(true, "Sending a message failed");
+    transport.close();
+    listener.close();
+    equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+    return;
+  }
+
+  do_throw("Connection unexpectedly succeeded");
+});
+
+// Client w/ invalid K value fails to connect
+add_task(function*() {
+  equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+  // Grab our cert, instead of relying on a discovery advertisement
+  let serverCert = yield cert.local.getOrCreate();
+
+  let oobData = promise.defer();
+  let AuthenticatorType = DebuggerServer.Authenticators.get("OOB_CERT");
+  let serverAuth = new AuthenticatorType.Server();
+  serverAuth.allowConnection = () => {
+    return DebuggerServer.AuthenticationResult.ALLOW;
+  };
+  serverAuth.receiveOOB = () => oobData.promise; // Skip prompt for tests
+
+  let clientAuth = new AuthenticatorType.Client();
+  clientAuth.sendOOB = ({ oob }) => {
+    do_print(oob);
+    do_print("Modifying K value, should fail");
+    // Pass to server, skipping prompt for tests
+    oobData.resolve({
+      k: oob.k + 1,
+      sha256: oob.sha256
+    });
+  };
+
+  let listener = DebuggerServer.createListener();
+  ok(listener, "Socket listener created");
+  listener.portOrPath = -1 /* any available port */;
+  listener.encryption = true /* required for OOB_CERT */;
+  listener.authenticator = serverAuth;
+  yield listener.open();
+  equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+  try {
+    yield DebuggerClient.socketConnect({
+      host: "127.0.0.1",
+      port: listener.port,
+      encryption: true,
+      authenticator: clientAuth,
+      cert: {
+        sha256: serverCert.sha256Fingerprint
+      }
+    });
+  } catch(e) {
+    ok(true, "Client failed to connect as expected");
+    listener.close();
+    equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+    return;
+  }
+
+  do_throw("Connection unexpectedly succeeded");
+});
+
+// Client w/ invalid cert hash fails to connect
+add_task(function*() {
+  equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+  // Grab our cert, instead of relying on a discovery advertisement
+  let serverCert = yield cert.local.getOrCreate();
+
+  let oobData = promise.defer();
+  let AuthenticatorType = DebuggerServer.Authenticators.get("OOB_CERT");
+  let serverAuth = new AuthenticatorType.Server();
+  serverAuth.allowConnection = () => {
+    return DebuggerServer.AuthenticationResult.ALLOW;
+  };
+  serverAuth.receiveOOB = () => oobData.promise; // Skip prompt for tests
+
+  let clientAuth = new AuthenticatorType.Client();
+  clientAuth.sendOOB = ({ oob }) => {
+    do_print(oob);
+    do_print("Modifying cert hash, should fail");
+    // Pass to server, skipping prompt for tests
+    oobData.resolve({
+      k: oob.k,
+      sha256: oob.sha256 + 1
+    });
+  };
+
+  let listener = DebuggerServer.createListener();
+  ok(listener, "Socket listener created");
+  listener.portOrPath = -1 /* any available port */;
+  listener.encryption = true /* required for OOB_CERT */;
+  listener.authenticator = serverAuth;
+  yield listener.open();
+  equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+  try {
+    yield DebuggerClient.socketConnect({
+      host: "127.0.0.1",
+      port: listener.port,
+      encryption: true,
+      authenticator: clientAuth,
+      cert: {
+        sha256: serverCert.sha256Fingerprint
+      }
+    });
+  } catch(e) {
+    ok(true, "Client failed to connect as expected");
+    listener.close();
+    equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+    return;
+  }
+
+  do_throw("Connection unexpectedly succeeded");
+});
+
+add_task(function*() {
+  DebuggerServer.destroy();
+});
--- a/toolkit/devtools/security/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/security/tests/unit/xpcshell.ini
@@ -3,8 +3,9 @@ head = head_dbg.js
 tail =
 skip-if = toolkit == 'android'
 
 support-files=
   testactors.js
 
 [test_cert.js]
 [test_encryption.js]
+[test_oob_cert_auth.js]
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -2628,16 +2628,21 @@ var WalkerActor = protocol.ActorClass({
     response: { attached: RetVal("boolean") }
   }),
 
   /**
    * Given an ObjectActor (identified by its ID), commonly used in the debugger,
    * webconsole and variablesView, return the corresponding inspector's NodeActor
    */
   getNodeActorFromObjectActor: method(function(objectActorID) {
+    let actor = this.conn.getActor(objectActorID);
+    if (!actor) {
+      return null;
+    }
+
     let debuggerObject = this.conn.getActor(objectActorID).obj;
     let rawNode = debuggerObject.unsafeDereference();
 
     if (!this._isInDOMTree(rawNode)) {
       return null;
     }
 
     // This is a special case for the document object whereby it is considered
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -19,16 +19,19 @@ let DevToolsUtils = require("devtools/to
 let { dumpn, dumpv, dbg_assert } = DevToolsUtils;
 let EventEmitter = require("devtools/toolkit/event-emitter");
 let Debugger = require("Debugger");
 
 DevToolsUtils.defineLazyGetter(this, "DebuggerSocket", () => {
   let { DebuggerSocket } = require("devtools/toolkit/security/socket");
   return DebuggerSocket;
 });
+DevToolsUtils.defineLazyGetter(this, "Authentication", () => {
+  return require("devtools/toolkit/security/auth");
+});
 
 // On B2G, `this` != Global scope, so `Ci` won't be binded on `this`
 // (i.e. this.Ci is undefined) Then later, when using loadSubScript,
 // Ci,... won't be defined for sub scripts.
 this.Ci = Ci;
 this.Cc = Cc;
 this.CC = CC;
 this.Cu = Cu;
@@ -1097,16 +1100,24 @@ var DebuggerServer = {
         for (let connID of Object.getOwnPropertyNames(this._connections)) {
           this._connections[connID].rootActor.removeActorByName(name);
         }
       }
     }
   }
 };
 
+// Expose these to save callers the trouble of importing DebuggerSocket
+DevToolsUtils.defineLazyGetter(DebuggerServer, "Authenticators", () => {
+  return Authentication.Authenticators;
+});
+DevToolsUtils.defineLazyGetter(DebuggerServer, "AuthenticationResult", () => {
+  return Authentication.AuthenticationResult;
+});
+
 EventEmitter.decorate(DebuggerServer);
 
 if (this.exports) {
   exports.DebuggerServer = DebuggerServer;
 }
 // Needed on B2G (See header note)
 this.DebuggerServer = DebuggerServer;
 
--- a/toolkit/devtools/transport/tests/unit/head_dbg.js
+++ b/toolkit/devtools/transport/tests/unit/head_dbg.js
@@ -256,19 +256,24 @@ function writeTestTempFile(aFileName, aC
     stream.close();
   }
 }
 
 /*** Transport Factories ***/
 
 let socket_transport = Task.async(function*() {
   if (!DebuggerServer.listeningSockets) {
+    let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
+    let authenticator = new AuthenticatorType.Server();
+    authenticator.allowConnection = () => {
+      return DebuggerServer.AuthenticationResult.ALLOW;
+    };
     let listener = DebuggerServer.createListener();
     listener.portOrPath = -1 /* any available port */;
-    listener.allowConnection = () => true;
+    listener.authenticator = authenticator;
     yield listener.open();
   }
   let port = DebuggerServer._listeners[0].port;
   do_print("Debugger server port is " + port);
   return DebuggerClient.socketConnect({ host: "127.0.0.1", port });
 });
 
 function local_transport() {
--- a/toolkit/devtools/transport/tests/unit/test_dbgsocket.js
+++ b/toolkit/devtools/transport/tests/unit/test_dbgsocket.js
@@ -17,28 +17,33 @@ function run_test()
   add_test(test_pipe_conn);
 
   run_next_test();
 }
 
 function* test_socket_conn()
 {
   do_check_eq(DebuggerServer.listeningSockets, 0);
+  let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
+  let authenticator = new AuthenticatorType.Server();
+  authenticator.allowConnection = () => {
+    return DebuggerServer.AuthenticationResult.ALLOW;
+  };
   let listener = DebuggerServer.createListener();
   do_check_true(listener);
   listener.portOrPath = -1 /* any available port */;
-  listener.allowConnection = () => true;
+  listener.authenticator = authenticator;
   listener.open();
   do_check_eq(DebuggerServer.listeningSockets, 1);
   gPort = DebuggerServer._listeners[0].port;
   do_print("Debugger server port is " + gPort);
   // Open a second, separate listener
   gExtraListener = DebuggerServer.createListener();
   gExtraListener.portOrPath = -1;
-  gExtraListener.allowConnection = () => true;
+  gExtraListener.authenticator = authenticator;
   gExtraListener.open();
   do_check_eq(DebuggerServer.listeningSockets, 2);
 
   do_print("Starting long and unicode tests at " + new Date().toTimeString());
   let unicodeString = "(╯°□°)╯︵ ┻━┻";
   let transport = yield DebuggerClient.socketConnect({
     host: "127.0.0.1",
     port: gPort
--- a/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js
+++ b/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js
@@ -42,19 +42,25 @@ function test_socket_conn_drops_after_to
   let rawPacket = '4305724038957487634549823475894325';
   for (let i = 0; i < 8; i++) {
     rawPacket += rawPacket;
   }
   return test_helper(rawPacket + ':');
 }
 
 let test_helper = Task.async(function*(payload) {
+  let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
+  let authenticator = new AuthenticatorType.Server();
+  authenticator.allowConnection = () => {
+    return DebuggerServer.AuthenticationResult.ALLOW;
+  };
+
   let listener = DebuggerServer.createListener();
   listener.portOrPath = -1;
-  listener.allowConnection = () => true;
+  listener.authenticator = authenticator;
   listener.open();
 
   let transport = yield DebuggerClient.socketConnect({
     host: "127.0.0.1",
     port: listener.port
   });
   let closedDeferred = promise.defer();
   transport.hooks = {
--- a/toolkit/locales/en-US/chrome/global/devtools/debugger.properties
+++ b/toolkit/locales/en-US/chrome/global/devtools/debugger.properties
@@ -9,16 +9,51 @@
 # You want to make that choice consistent across the developer tools.
 # A good criteria is the language in which you'd find the best
 # documentation on web development on the web.
 
 # LOCALIZATION NOTE (remoteIncomingPromptTitle): The title displayed on the
 # dialog that prompts the user to allow the incoming connection.
 remoteIncomingPromptTitle=Incoming Connection
 
-# LOCALIZATION NOTE (remoteIncomingPromptMessage): The message displayed on the
+# LOCALIZATION NOTE (remoteIncomingPromptHeader): Header displayed on the
 # dialog that prompts the user to allow the incoming connection.
-remoteIncomingPromptMessage=An incoming request to permit remote debugging connection was detected. A remote client can take complete control over your browser! Allow connection?
+remoteIncomingPromptHeader=An incoming request to permit remote debugging connection was detected. A remote client can take complete control over your browser!
+# LOCALIZATION NOTE (remoteIncomingPromptClientEndpoint): Part of the prompt
+# dialog for the user to choose whether an incoming connection should be
+# allowed.
+# %1$S: The host and port of the client such as "127.0.0.1:6000"
+remoteIncomingPromptClientEndpoint=Client Endpoint: %1$S
+# LOCALIZATION NOTE (remoteIncomingPromptServerEndpoint): Part of the prompt
+# dialog for the user to choose whether an incoming connection should be
+# allowed.
+# %1$S: The host and port of the server such as "127.0.0.1:6000"
+remoteIncomingPromptServerEndpoint=Server Endpoint: %1$S
+# LOCALIZATION NOTE (remoteIncomingPromptFooter): Footer displayed on the
+# dialog that prompts the user to allow the incoming connection.
+remoteIncomingPromptFooter=Allow connection?
 
 # LOCALIZATION NOTE (remoteIncomingPromptDisable): The label displayed on the
 # third button in the incoming connection dialog that lets the user disable the
 # remote debugger server.
 remoteIncomingPromptDisable=Disable
+
+# LOCALIZATION NOTE (clientSendOOBTitle): The title displayed on the dialog that
+# instructs the user to transfer an authentication token to the server.
+clientSendOOBTitle=Client Identification
+# LOCALIZATION NOTE (clientSendOOBHeader): Header displayed on the dialog that
+# instructs the user to transfer an authentication token to the server.
+clientSendOOBHeader=The endpoint you are connecting to needs more information to authenticate this connection.  Please provide the token below in the prompt that appears on the other end.
+# LOCALIZATION NOTE (clientSendOOBHash): Part of the dialog that instructs the
+# user to transfer an authentication token to the server.
+# %1$S: The client's cert fingerprint
+clientSendOOBHash=My Cert: %1$S
+# LOCALIZATION NOTE (clientSendOOBToken): Part of the dialog that instructs the
+# user to transfer an authentication token to the server.
+# %1$S: The authentication token that the user will transfer.
+clientSendOOBToken=Token: %1$S
+
+# LOCALIZATION NOTE (serverReceiveOOBTitle): The title displayed on the dialog
+# that instructs the user to provide an authentication token from the client.
+serverReceiveOOBTitle=Provide Client Token
+# LOCALIZATION NOTE (serverReceiveOOBBody): Main text displayed on the dialog
+# that instructs the user to provide an authentication token from the client.
+serverReceiveOOBBody=The client should be displaying a token value.  Enter that token value here to complete authentication with this client.
--- a/toolkit/themes/shared/in-content/common.inc.css
+++ b/toolkit/themes/shared/in-content/common.inc.css
@@ -636,17 +636,17 @@ xul|treecol:not([hideheader="true"]),
 xul|treecolpicker {
   -moz-appearance: none;
   border: none;
   background-color: #ebebeb;
   color: #808080;
   padding: 5px 10px;
 }
 
-xul|treecol:not([hideheader="true"]):hover,
+xul|treecol:not([hideheader="true"]):not([sortable="false"]):hover,
 xul|treecolpicker:hover {
   background-color: #dadada;
   color: #333;
 }
 
 xul|treecol:not([hideheader="true"]):not(:first-child),
 xul|treecolpicker {
   -moz-border-start-width: 1px;
@@ -677,9 +677,9 @@ xul|treechildren::-moz-tree-row {
 
 /* Color needs to be set on tree cell in order to be applied */
 xul|treechildren::-moz-tree-cell-text {
   color: #333;
 }
 
 xul|treechildren::-moz-tree-cell-text(selected) {
   color: #fff;
-}
\ No newline at end of file
+}