Bug 876277 - Rewrite head.js to use promises and remove useless cruft, r=past
authorVictor Porof <vporof@mozilla.com>
Fri, 13 Sep 2013 16:23:16 +0300
changeset 147079 1f66a755177f5c0a34be410e39b4adab0590d9c1
parent 147078 04c318f79c84e50a8a4c170e3b728b313adf956b
child 147080 c55396293b49f4fc43f6380bac9c63b1b58885ce
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerspast
bugs876277
milestone26.0a1
Bug 876277 - Rewrite head.js to use promises and remove useless cruft, r=past
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/test/head.js
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -1709,43 +1709,25 @@ DebuggerController.ThreadState = new Thr
 DebuggerController.StackFrames = new StackFrames();
 DebuggerController.SourceScripts = new SourceScripts();
 DebuggerController.Breakpoints = new Breakpoints();
 
 /**
  * Export some properties to the global scope for easier access.
  */
 Object.defineProperties(window, {
-  "dispatchEvent": {
-    get: function() ViewHelpers.dispatchEvent,
-  },
-  "editor": {
-    get: function() DebuggerView.editor
-  },
   "gTarget": {
     get: function() DebuggerController._target
   },
   "gClient": {
     get: function() DebuggerController.client
   },
   "gThreadClient": {
     get: function() DebuggerController.activeThread
   },
-  "gThreadState": {
-    get: function() DebuggerController.ThreadState
-  },
-  "gStackFrames": {
-    get: function() DebuggerController.StackFrames
-  },
-  "gSourceScripts": {
-    get: function() DebuggerController.SourceScripts
-  },
-  "gBreakpoints": {
-    get: function() DebuggerController.Breakpoints
-  },
   "gCallStackPageSize": {
     get: function() CALL_STACK_PAGE_SIZE
   }
 });
 
 /**
  * Helper method for debugging.
  * @param string
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -1,247 +1,508 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 
-let tempScope = {};
-Cu.import("resource://gre/modules/Services.jsm", tempScope);
-let Services = tempScope.Services;
 // Disable logging for faster test runs. Set this pref to true if you want to
-// debug a test in your try runs.
+// debug a test in your try runs. Both the debugger server and frontend will
+// be affected by this pref.
 let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
 Services.prefs.setBoolPref("devtools.debugger.log", false);
 
-Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope);
-Cu.import("resource://gre/modules/devtools/dbg-client.jsm", tempScope);
-Cu.import("resource:///modules/source-editor.jsm", tempScope);
-Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope);
-Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope);
-Cu.import("resource://gre/modules/AddonManager.jsm", tempScope);
-let SourceEditor = tempScope.SourceEditor;
-let DebuggerServer = tempScope.DebuggerServer;
-let DebuggerTransport = tempScope.DebuggerTransport;
-let DebuggerClient = tempScope.DebuggerClient;
-let AddonManager = tempScope.AddonManager;
-let gDevTools = tempScope.gDevTools;
-let devtools = tempScope.devtools;
+let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+let { Promise: promise } = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let { BrowserDebuggerProcess } = Cu.import("resource:///modules/devtools/DebuggerProcess.jsm", {});
+let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
+let { SourceEditor } = Cu.import("resource:///modules/source-editor.jsm", {});
+let { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
 let TargetFactory = devtools.TargetFactory;
+let Toolbox = devtools.Toolbox;
+
+const EXAMPLE_URL = "http://example.com/browser/browser/devtools/debugger/test/";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+registerCleanupFunction(function() {
+  info("finish() was called, cleaning up...");
+  Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+
+  // Properly shut down the server to avoid memory leaks.
+  DebuggerServer.destroy();
+});
 
 // Import the GCLI test helper
 let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
 Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this);
 
-const EXAMPLE_URL = "http://example.com/browser/browser/devtools/debugger/test/";
-const TAB1_URL = EXAMPLE_URL + "browser_dbg_tab1.html";
-const TAB2_URL = EXAMPLE_URL + "browser_dbg_tab2.html";
-const ADDON1_URL = EXAMPLE_URL + "browser_dbg_addon1.xpi";
-const ADDON2_URL = EXAMPLE_URL + "browser_dbg_addon2.xpi";
-const STACK_URL = EXAMPLE_URL + "browser_dbg_stack.html";
-
-// Enable remote debugging for the relevant tests.
-let gEnableRemote = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
-Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
-
 // Redeclare dbg_assert with a fatal behavior.
 function dbg_assert(cond, e) {
   if (!cond) {
     throw e;
   }
 }
 
-registerCleanupFunction(function() {
-  Services.prefs.setBoolPref("devtools.debugger.remote-enabled", gEnableRemote);
-  Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
-
-  // Properly shut down the server to avoid memory leaks.
-  DebuggerServer.destroy();
-});
-
-if (!DebuggerServer.initialized) {
-  DebuggerServer.init(function() true);
-  DebuggerServer.addBrowserActors();
+function addWindow(aUrl) {
+  info("Adding window: " + aUrl);
+  return promise.resolve(getDOMWindow(window.open(aUrl)));
 }
 
-waitForExplicitFinish();
-
-function addWindow() {
-  let windowReference = window.open();
-  let chromeWindow = windowReference
+function getDOMWindow(aReference) {
+  return aReference
     .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
     .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
     .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
-
-  return chromeWindow;
 }
 
-function addTab(aURL, aOnload, aWindow) {
+function addTab(aUrl, aWindow) {
+  info("Adding tab: " + aUrl);
+
+  let deferred = promise.defer();
   let targetWindow = aWindow || window;
   let targetBrowser = targetWindow.gBrowser;
 
   targetWindow.focus();
-  targetBrowser.selectedTab = targetBrowser.addTab(aURL);
-
-  let tab = targetBrowser.selectedTab;
-  let browser = tab.linkedBrowser;
-  let win = browser.contentWindow;
-  let expectedReadyState = aURL == "about:blank" ? ["interactive", "complete"] : ["complete"];
+  let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+  let linkedBrowser = tab.linkedBrowser;
 
-  if (aOnload) {
-    let handler = function() {
-      if (browser.currentURI.spec != aURL ||
-          expectedReadyState.indexOf((win.document || {}).readyState) == -1) {
-        return;
-      }
-      browser.removeEventListener("load", handler, true);
-      executeSoon(aOnload);
-    }
-    browser.addEventListener("load", handler, true);
-  }
+  linkedBrowser.addEventListener("load", function onLoad() {
+    linkedBrowser.removeEventListener("load", onLoad, true);
+    deferred.resolve(tab);
+  }, true);
 
-  return tab;
+  return deferred.promise;
 }
 
 function removeTab(aTab, aWindow) {
+  info("Removing tab.");
+
+  let deferred = promise.defer();
   let targetWindow = aWindow || window;
   let targetBrowser = targetWindow.gBrowser;
+  let tabContainer = targetBrowser.tabContainer;
+
+  tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+    tabContainer.removeEventListener("TabClose", onClose, false);
+    deferred.resolve();
+  }, false);
 
   targetBrowser.removeTab(aTab);
+  return deferred.promise;
 }
 
-function addAddon(aURL, aOnInstallEnded) {
-  AddonManager.getInstallForURL(aURL, function(aInstall) {
-    aInstall.install();
-    var listener = {
+function addAddon(aUrl) {
+  info("Installing addon: " + aUrl);
+
+  let deferred = promise.defer();
+
+  AddonManager.getInstallForURL(aUrl, aInstaller => {
+    aInstaller.install();
+    let listener = {
       onInstallEnded: function(aAddon, aAddonInstall) {
-        aInstall.removeListener(listener);
-        aOnInstallEnded(aAddonInstall);
+        aInstaller.removeListener(listener);
+        deferred.resolve(aAddonInstall);
       }
     };
-    aInstall.addListener(listener);
+    aInstaller.addListener(listener);
   }, "application/x-xpinstall");
+
+  return deferred.promise;
 }
 
-function removeAddon(aAddon, aOnUninstalled) {
-  var listener = {
+function removeAddon(aAddon) {
+  info("Removing addon.");
+
+  let deferred = promise.defer();
+
+  let listener = {
     onUninstalled: function(aUninstalledAddon) {
-      if (aUninstalledAddon != aAddon)
+      if (aUninstalledAddon != aAddon) {
         return;
+      }
       AddonManager.removeAddonListener(listener);
-      aOnUninstalled();
+      deferred.resolve();
     }
   };
   AddonManager.addAddonListener(listener);
   aAddon.uninstall();
+
+  return deferred.promise;
+}
+
+function getTabActorForUrl(aClient, aUrl) {
+  let deferred = promise.defer();
+
+  aClient.listTabs(aResponse => {
+    let tabActor = aResponse.tabs.filter(aGrip => aGrip.url == aUrl).pop();
+    deferred.resolve(tabActor);
+  });
+
+  return deferred.promise;
+}
+
+function getAddonActorForUrl(aClient, aUrl) {
+  let deferred = promise.defer();
+
+  aClient.listAddons(aResponse => {
+    let addonActor = aResponse.addons.filter(aGrip => aGrip.url == aUrl).pop();
+    deferred.resolve(addonActor);
+  });
+
+  return deferred.promise;
+}
+
+function attachTabActorForUrl(aClient, aUrl) {
+  let deferred = promise.defer();
+
+  getTabActorForUrl(aClient, aUrl).then(aGrip => {
+    aClient.attachTab(aGrip.actor, aResponse => {
+      deferred.resolve([aGrip, aResponse]);
+    });
+  });
+
+  return deferred.promise;
+}
+
+function attachThreadActorForUrl(aClient, aUrl) {
+  let deferred = promise.defer();
+
+  attachTabActorForUrl(aClient, aUrl).then(([aGrip, aResponse]) => {
+    aClient.attachThread(aResponse.threadActor, (aResponse, aThreadClient) => {
+      aThreadClient.resume(aResponse => {
+        deferred.resolve(aThreadClient);
+      });
+    });
+  });
+
+  return deferred.promise;
 }
 
-function closeDebuggerAndFinish(aRemoteFlag, aCallback, aWindow) {
-  let debuggerClosed = false;
-  let debuggerDisconnected = false;
+function once(aTarget, aEventName, aUseCapture = false) {
+  info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
 
-  ok(gTab, "There is a gTab to use for getting a toolbox reference");
-  let target = TargetFactory.forTab(gTab);
+  let deferred = promise.defer();
 
-  window.addEventListener("Debugger:Shutdown", function cleanup() {
-    window.removeEventListener("Debugger:Shutdown", cleanup, false);
-    debuggerDisconnected = true;
-    maybeFinish();
-  }, false);
-
-  let toolbox = gDevTools.getToolbox(target);
-  toolbox.destroy().then(function() {
-    debuggerClosed = true;
-    maybeFinish();
-  });
-
-  function maybeFinish() {
-    if (debuggerClosed && debuggerDisconnected) {
-      (finish || aCallback)();
+  for (let [add, remove] of [
+    ["addEventListener", "removeEventListener"],
+    ["addListener", "removeListener"],
+    ["on", "off"]
+  ]) {
+    if ((add in aTarget) && (remove in aTarget)) {
+      aTarget[add](aEventName, function onEvent(...aArgs) {
+        aTarget[remove](aEventName, onEvent, aUseCapture);
+        deferred.resolve.apply(deferred, aArgs);
+      }, aUseCapture);
+      break;
     }
   }
+
+  return deferred.promise;
+}
+
+function waitForSourceShown(aPanel, aUrl) {
+  return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.SOURCE_SHOWN).then(aSource => {
+    let sourceUrl = aSource.url;
+    info("Source shown: " + sourceUrl);
+
+    if (!sourceUrl.contains(aUrl)) {
+      return waitForSourceShown(aPanel, aUrl);
+    } else {
+      ok(true, "The correct source has been shown.");
+    }
+  });
 }
 
-function get_tab_actor_for_url(aClient, aURL, aCallback) {
-  aClient.listTabs(function(aResponse) {
-    for each (let tab in aResponse.tabs) {
-      if (tab.url == aURL) {
-        aCallback(tab);
-        return;
-      }
+function ensureSourceIs(aPanel, aUrl, aWaitFlag = false) {
+  if (aPanel.panelWin.DebuggerView.Sources.selectedValue.contains(aUrl)) {
+    ok(true, "Expected source is shown: " + aUrl);
+    return promise.resolve(null);
+  }
+  if (aWaitFlag) {
+    return waitForSourceShown(aPanel, aUrl);
+  }
+  ok(false, "Expected source was not already shown: " + aUrl);
+  return promise.reject(null);
+}
+
+function waitForCaretUpdated(aPanel, aLine, aCol = 1) {
+  return waitForEditorEvents(aPanel, SourceEditor.EVENTS.SELECTION).then(() => {
+    let caret = aPanel.panelWin.DebuggerView.editor.getCaretPosition();
+    info("Caret updated: " + (caret.line + 1) + ", " + (caret.col + 1));
+
+    if (!isCaretPos(aPanel, aLine, aCol)) {
+      return waitForCaretUpdated(aPanel, aLine, aCol);
+    } else {
+      ok(true, "The correct caret position has been set.");
     }
   });
 }
 
-function attach_tab_actor_for_url(aClient, aURL, aCallback) {
-  get_tab_actor_for_url(aClient, aURL, function(actor) {
-    aClient.attachTab(actor.actor, function(aResponse) {
-      aCallback(actor, aResponse);
-    });
+function ensureCaretAt(aPanel, aLine, aCol = 1, aWaitFlag = false) {
+  if (isCaretPos(aPanel, aLine, aCol)) {
+    ok(true, "Expected caret position is set: " + aLine + "," + aCol);
+    return promise.resolve(null);
+  }
+  if (aWaitFlag) {
+    return waitForCaretUpdated(aPanel, aLine, aCol);
+  }
+  ok(false, "Expected caret position was not already set: " + aLine + "," + aCol);
+  return promise.reject(null);
+}
+
+function isCaretPos(aPanel, aLine, aCol = 1) {
+  let editor = aPanel.panelWin.DebuggerView.editor;
+  let caret = editor.getCaretPosition();
+
+  // Source editor starts counting line and column numbers from 0.
+  info("Current editor caret position: " + (caret.line + 1) + ", " + (caret.col + 1));
+  return caret.line == (aLine - 1) && caret.col == (aCol - 1);
+}
+
+function isEditorSel(aPanel, [start, end]) {
+  let editor = aPanel.panelWin.DebuggerView.editor;
+  let range = editor.getSelection();
+
+  // Source editor starts counting line and column numbers from 0.
+  info("Current editor selection: " + (range.start + 1) + ", " + (range.end + 1));
+  return range.start == (start - 1) && range.end == (end - 1);
+}
+
+function waitForSourceAndCaret(aPanel, aUrl, aLine, aCol) {
+  return promise.all([
+    waitForSourceShown(aPanel, aUrl),
+    waitForCaretUpdated(aPanel, aLine, aCol)
+  ]);
+}
+
+function waitForCaretAndScopes(aPanel, aLine, aCol) {
+  return promise.all([
+    waitForCaretUpdated(aPanel, aLine, aCol),
+    waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.FETCHED_SCOPES)
+  ]);
+}
+
+function waitForSourceAndCaretAndScopes(aPanel, aUrl, aLine, aCol) {
+  return promise.all([
+    waitForSourceAndCaret(aPanel, aUrl, aLine, aCol),
+    waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.FETCHED_SCOPES)
+  ]);
+}
+
+function waitForDebuggerEvents(aPanel, aEventName, aEventRepeat = 1) {
+  info("Waiting for debugger event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s).");
+
+  let deferred = promise.defer();
+  let panelWin = aPanel.panelWin;
+  let count = 0;
+
+  panelWin.on(aEventName, function onEvent(aEventName, ...aArgs) {
+    info("Debugger event '" + aEventName + "' fired: " + (++count) + " time(s).");
+
+    if (count == aEventRepeat) {
+      ok(true, "Enough '" + aEventName + "' panel events have been fired.");
+      panelWin.off(aEventName, onEvent);
+      deferred.resolve.apply(deferred, aArgs);
+    }
   });
+
+  return deferred.promise;
+}
+
+function waitForEditorEvents(aPanel, aEventName, aEventRepeat = 1) {
+  info("Waiting for editor event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s).");
+
+  let deferred = promise.defer();
+  let editor = aPanel.panelWin.DebuggerView.editor;
+  let count = 0;
+
+  editor.addEventListener(aEventName, function onEvent(...aArgs) {
+    info("Editor event '" + aEventName + "' fired: " + (++count) + " time(s).");
+
+    if (count == aEventRepeat) {
+      ok(true, "Enough '" + aEventName + "' editor events have been fired.");
+      editor.removeEventListener(aEventName, onEvent);
+      deferred.resolve.apply(deferred, aArgs);
+    }
+  });
+
+  return deferred.promise;
 }
 
-function attach_thread_actor_for_url(aClient, aURL, aCallback) {
-  attach_tab_actor_for_url(aClient, aURL, function(aTabActor, aResponse) {
-    aClient.attachThread(aResponse.threadActor, function(aResponse, aThreadClient) {
-      // We don't care about the pause right now (use
-      // get_actor_for_url() if you do), so resume it.
-      aThreadClient.resume(function(aResponse) {
-        aCallback(aThreadClient);
+function waitForThreadEvents(aPanel, aEventName, aEventRepeat = 1) {
+  info("Waiting for thread event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s).");
+
+  let deferred = promise.defer();
+  let thread = aPanel.panelWin.gThreadClient;
+  let count = 0;
+
+  thread.addListener(aEventName, function onEvent(aEventName, ...aArgs) {
+    info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s).");
+
+    if (count == aEventRepeat) {
+      ok(true, "Enough '" + aEventName + "' thread events have been fired.");
+      thread.removeListener(aEventName, onEvent);
+      deferred.resolve.apply(deferred, aArgs);
+    }
+  });
+
+  return deferred.promise;
+}
+
+function ensureThreadClientState(aPanel, aState) {
+  let thread = aPanel.panelWin.gThreadClient;
+  let state = thread.state;
+
+  info("Thread is: '" + state + "'.");
+
+  if (state == aState) {
+    return promise.resolve(null);
+  } else {
+    return waitForThreadEvents(aPanel, aState);
+  }
+}
+
+function navigateActiveTabTo(aPanel, aUrl, aWaitForEventName, aEventRepeat) {
+  let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat);
+  let activeTab = aPanel.panelWin.gClient.activeTab;
+  aUrl ? activeTab.navigateTo(aUrl) : activeTab.reload();
+  return finished;
+}
+
+function navigateActiveTabInHistory(aPanel, aDirection, aWaitForEventName, aEventRepeat) {
+  let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat);
+  content.history[aDirection]();
+  return finished;
+}
+
+function reloadActiveTab(aPanel, aWaitForEventName, aEventRepeat) {
+  return navigateActiveTabTo(aPanel, null, aWaitForEventName, aEventRepeat);
+}
+
+function clearText(aElement) {
+  info("Clearing text...");
+  aElement.focus();
+  aElement.value = "";
+}
+
+function setText(aElement, aText) {
+  clearText(aElement);
+  info("Setting text: " + aText);
+  aElement.value = aText;
+}
+
+function typeText(aElement, aText) {
+  info("Typing text: " + aText);
+  aElement.focus();
+  EventUtils.sendString(aText, aElement.ownerDocument.defaultView);
+}
+
+function backspaceText(aElement, aTimes) {
+  info("Pressing backspace " + aTimes + " times.");
+  for (let i = 0; i < aTimes; i++) {
+    aElement.focus();
+    EventUtils.sendKey("BACK_SPACE", aElement.ownerDocument.defaultView);
+  }
+}
+
+function getTab(aTarget) {
+  if (aTarget instanceof XULElement) {
+    return promise.resolve(aTarget);
+  } else {
+    return addTab(aTarget);
+  }
+}
+
+function initDebugger(aTarget, aWindow) {
+  info("Initializing a debugger panel.");
+
+  return getTab(aTarget).then(aTab => {
+    info("Debugee tab added successfully: " + aTarget);
+
+    let deferred = promise.defer();
+    let debuggee = aTab.linkedBrowser.contentWindow.wrappedJSObject;
+    let target = TargetFactory.forTab(aTab);
+
+    gDevTools.showToolbox(target, "jsdebugger").then(aToolbox => {
+      info("Debugger panel shown successfully.");
+
+      let debuggerPanel = aToolbox.getCurrentPanel();
+      let panelWin = debuggerPanel.panelWin;
+
+      // Wait for the initial resume...
+      panelWin.gClient.addOneTimeListener("resumed", () => {
+        info("Debugger client resumed successfully.");
+
+        prepareDebugger(debuggerPanel);
+        deferred.resolve([aTab, debuggee, debuggerPanel]);
       });
     });
-  });
-}
 
-function wait_for_connect_and_resume(aOnDebugging, aTab) {
-  let target = TargetFactory.forTab(aTab);
-
-  gDevTools.showToolbox(target, "jsdebugger").then(function(toolbox) {
-    let dbg = toolbox.getCurrentPanel();
-
-    // Wait for the initial resume...
-    dbg.panelWin.gClient.addOneTimeListener("resumed", function() {
-      aOnDebugging();
-    });
+    return deferred.promise;
   });
 }
 
-function debug_tab_pane(aURL, aOnDebugging, aBeforeTabAdded) {
-  // Make any necessary preparations (start the debugger server etc.)
-  if (aBeforeTabAdded) {
-    aBeforeTabAdded();
-  }
+function initChromeDebugger(aOnClose) {
+  info("Initializing a chrome debugger process.");
+
+  let deferred = promise.defer();
 
-  let tab = addTab(aURL, function() {
-    let debuggee = gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject;
-    let target = TargetFactory.forTab(gBrowser.selectedTab);
+  // Wait for the debugger process to start...
+  BrowserDebuggerProcess.init(aOnClose, aProcess => {
+    info("Chrome debugger process started successfully.");
+
+    prepareDebugger(aProcess);
+    deferred.resolve(aProcess);
+  });
 
-    info("Opening Debugger");
-    gDevTools.showToolbox(target, "jsdebugger").then(function(toolbox) {
-      let dbg = toolbox.getCurrentPanel();
+  return deferred.promise;
+}
 
-      // Wait for the initial resume...
-      dbg.panelWin.gClient.addOneTimeListener("resumed", function() {
-        info("Debugger has started");
-        dbg._view.Variables.lazyEmpty = false;
-        dbg._view.Variables.lazyAppend = false;
-        dbg._view.Variables.lazyExpand = false;
-        aOnDebugging(tab, debuggee, dbg);
-      });
-    });
-  });
+function prepareDebugger(aDebugger) {
+  if ("target" in aDebugger) {
+    let variables = aDebugger.panelWin.DebuggerView.Variables;
+    variables.lazyEmpty = false;
+    variables.lazyAppend = false;
+    variables.lazyExpand = false;
+    variables.lazySearch = false;
+  } else {
+    // Nothing to do here yet.
+  }
 }
 
-function debug_chrome(aURL, aOnClosing, aOnDebugging) {
-  let tab = addTab(aURL, function() {
-    let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject;
+function teardown(aPanel, aFlags = {}) {
+  info("Destroying the specified debugger.");
 
-    info("Opening Browser Debugger");
-    let win = BrowserDebuggerProcess.init(aOnClosing, function(process) {
+  let toolbox = aPanel._toolbox;
+  let tab = aPanel.target.tab;
+  let debuggerRootActorDisconnected = once(window, "Debugger:Shutdown");
+  let debuggerPanelDestroyed = once(aPanel, "destroyed");
+  let devtoolsToolboxDestroyed = toolbox.destroy();
+
+  return promise.all([
+    debuggerRootActorDisconnected,
+    debuggerPanelDestroyed,
+    devtoolsToolboxDestroyed
+  ]).then(() => aFlags.noTabRemoval ? null : removeTab(tab));
+}
 
-      // The remote debugging process has started...
-      info("Browser Debugger has started");
-      aOnDebugging(tab, debuggee, process);
-    });
-  });
+function closeDebuggerAndFinish(aPanel, aFlags = {}) {
+  let thread = aPanel.panelWin.gThreadClient;
+  if (thread.state == "paused" && !aFlags.whilePaused) {
+    ok(false, "You should use 'resumeDebuggerThenCloseAndFinish' instead, " +
+              "unless you're absolutely sure about what you're doing.");
+  }
+  return teardown(aPanel, aFlags).then(finish);
 }
+
+function resumeDebuggerThenCloseAndFinish(aPanel, aFlags = {}) {
+  let deferred = promise.defer();
+  let thread = aPanel.panelWin.gThreadClient;
+  thread.resume(() => closeDebuggerAndFinish(aPanel, aFlags).then(deferred.resolve));
+  return deferred.promise;
+}