Bug 710258 - Don't allow the debugger to be open in more than one window; r=past
authorVictor Porof <vporof@mozilla.com>
Wed, 20 Jun 2012 15:21:46 +0300
changeset 101930 6cbfdf764d22f79f47e2a8470352beaf22f3780b
parent 101929 576e10abf8248f793c1cf9b1c3ae93526923cfa3
child 101931 32975daf82d78efa6aafd7d00ef38ccec0b18161
push id1316
push userakeybl@mozilla.com
push dateMon, 27 Aug 2012 22:37:00 +0000
treeherdermozilla-beta@db4b09302ee2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast
bugs710258
milestone16.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 710258 - Don't allow the debugger to be open in more than one window; r=past
browser/devtools/debugger/DebuggerUI.jsm
browser/devtools/debugger/test/Makefile.in
browser/devtools/debugger/test/browser_dbg_debugger-tab-switch-window.js
browser/devtools/debugger/test/browser_dbg_debugger-tab-switch.js
browser/devtools/debugger/test/head.js
--- a/browser/devtools/debugger/DebuggerUI.jsm
+++ b/browser/devtools/debugger/DebuggerUI.jsm
@@ -17,17 +17,17 @@ const TAB_SWITCH_NOTIFICATION = "debugge
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 let EXPORTED_SYMBOLS = ["DebuggerUI"];
 
 /**
- * Provides a simple mechanism of managing debugger instances per tab.
+ * Provides a simple mechanism of managing debugger instances.
  *
  * @param nsIDOMWindow aWindow
  *        The chrome window for which the DebuggerUI instance is created.
  */
 function DebuggerUI(aWindow) {
   this.chromeWindow = aWindow;
   this.listenToTabs();
 }
@@ -66,17 +66,17 @@ DebuggerUI.prototype = {
     }
   },
 
   /**
    * Starts a debugger for the current tab, or stops it if already started.
    * @return DebuggerPane if the debugger is started, null if it's stopped.
    */
   toggleDebugger: function DUI_toggleDebugger() {
-    let scriptDebugger = this.getDebugger();
+    let scriptDebugger = this.findDebugger();
     let selectedTab = this.chromeWindow.gBrowser.selectedTab;
 
     if (scriptDebugger) {
       if (scriptDebugger.ownerTab !== selectedTab) {
         this.showTabSwitchNotification();
         return scriptDebugger;
       }
       scriptDebugger.close();
@@ -105,44 +105,65 @@ DebuggerUI.prototype = {
    */
   toggleChromeDebugger: function DUI_toggleChromeDebugger(aOnClose, aOnRun) {
     let chromeDebugger = this.getChromeDebugger();
 
     if (chromeDebugger) {
       chromeDebugger.close();
       return null;
     }
-    return new ChromeDebuggerProcess(this.chromeWindow, aOnClose, aOnRun, true);
+    return new ChromeDebuggerProcess(this, aOnClose, aOnRun);
+  },
+
+  /**
+   * Gets the script debugger in any open window.
+   *
+   * @return DebuggerPane | null
+   *         The script debugger instance if it exists, null otherwise.
+   */
+  findDebugger: function DUI_findDebugger() {
+    let enumerator = Services.wm.getEnumerator("navigator:browser");
+    while (enumerator.hasMoreElements()) {
+      let chromeWindow = enumerator.getNext().QueryInterface(Ci.nsIDOMWindow);
+      let scriptDebugger = chromeWindow.DebuggerUI.getDebugger();
+      if (scriptDebugger) {
+        return scriptDebugger;
+      }
+    }
+    return null;
   },
 
   /**
    * Get the current script debugger.
-   * @return DebuggerPane if a debugger exists for the tab, null otherwise.
+   *
+   * @return DebuggerPane | null
+   *         The script debugger instance if it exists, null otherwise.
    */
   getDebugger: function DUI_getDebugger() {
-    let win = this.chromeWindow;
-    return '_scriptDebugger' in win ? win._scriptDebugger : null;
+    return '_scriptDebugger' in this ? this._scriptDebugger : null;
   },
 
   /**
    * Get the remote debugger for the current chrome window.
-   * @return RemoteDebuggerWindow if a remote debugger exists, null otherwise.
+   *
+   * @return RemoteDebuggerWindow | null
+   *         The remote debugger instance if it exists, null otherwise.
    */
   getRemoteDebugger: function DUI_getRemoteDebugger() {
-    let win = this.chromeWindow;
-    return '_remoteDebugger' in win ? win._remoteDebugger : null;
+    return '_remoteDebugger' in this ? this._remoteDebugger : null;
   },
 
   /**
    * Get the chrome debugger for the current firefox instance.
-   * @return ChromeDebuggerProcess if a chrome debugger exists, null otherwise.
+   *
+   * @return ChromeDebuggerProcess | null
+   *         The chrome debugger instance if it exists, null otherwise.
    */
   getChromeDebugger: function DUI_getChromeDebugger() {
-    let win = this.chromeWindow;
-    return '_chromeDebugger' in win ? win._chromeDebugger : null;
+    return '_chromeDebugger' in this ? this._chromeDebugger : null;
   },
 
   /**
    * Get the preferences associated with the debugger frontend.
    * @return object
    */
   get preferences() {
     return DebuggerPreferences;
@@ -165,24 +186,27 @@ DebuggerUI.prototype = {
       return;
     }
 
     let buttons = [{
       id: "debugger.confirmTabSwitch.buttonSwitch",
       label: L10N.getStr("confirmTabSwitch.buttonSwitch"),
       accessKey: L10N.getStr("confirmTabSwitch.buttonSwitch.accessKey"),
       callback: function DUI_notificationButtonSwitch() {
-        gBrowser.selectedTab = this.getDebugger().ownerTab;
+        let scriptDebugger = this.findDebugger();
+        let targetWindow = scriptDebugger.globalUI.chromeWindow;
+        targetWindow.gBrowser.selectedTab = scriptDebugger.ownerTab;
+        targetWindow.focus();
       }.bind(this)
     }, {
       id: "debugger.confirmTabSwitch.buttonOpen",
       label: L10N.getStr("confirmTabSwitch.buttonOpen"),
       accessKey: L10N.getStr("confirmTabSwitch.buttonOpen.accessKey"),
       callback: function DUI_notificationButtonOpen() {
-        this.getDebugger().close();
+        this.findDebugger().close();
         this.toggleDebugger();
       }.bind(this)
     }];
 
     let message = L10N.getStr("confirmTabSwitch.message");
     let imageURL = "chrome://browser/skin/Info.png";
 
     notification = nbox.appendNotification(
@@ -199,17 +223,17 @@ DebuggerUI.prototype = {
  * Creates a pane that will host the debugger.
  *
  * @param DebuggerUI aDebuggerUI
  *        The parent instance creating the new debugger.
  * @param XULElement aTab
  *        The tab in which to create the debugger.
  */
 function DebuggerPane(aDebuggerUI, aTab) {
-  this._globalUI = aDebuggerUI;
+  this.globalUI = aDebuggerUI;
   this._win = aDebuggerUI.chromeWindow;
   this._tab = aTab;
 
   this._initServer();
   this._create();
 }
 
 DebuggerPane.prototype = {
@@ -224,17 +248,17 @@ DebuggerPane.prototype = {
       DebuggerServer.addBrowserActors();
     }
   },
 
   /**
    * Creates and initializes the widgets containing the debugger UI.
    */
   _create: function DP__create() {
-    this._win._scriptDebugger = this;
+    this.globalUI._scriptDebugger = this;
 
     let gBrowser = this._win.gBrowser;
     let ownerDocument = gBrowser.parentNode.ownerDocument;
 
     this._splitter = ownerDocument.createElement("splitter");
     this._splitter.setAttribute("class", "devtools-horizontal-splitter");
 
     this._frame = ownerDocument.createElement("iframe");
@@ -255,31 +279,31 @@ DebuggerPane.prototype = {
       // Bind shortcuts for accessing the breakpoint methods in the debugger.
       let bkp = self.contentWindow.DebuggerController.Breakpoints;
       self.addBreakpoint = bkp.addBreakpoint;
       self.removeBreakpoint = bkp.removeBreakpoint;
       self.getBreakpoint = bkp.getBreakpoint;
     }, true);
 
     this._frame.setAttribute("src", DBG_XUL);
-    this._globalUI.refreshCommand();
+    this.globalUI.refreshCommand();
   },
 
   /**
    * Closes the debugger, removing child nodes and event listeners.
    *
    * @param function aCloseCallback
    *        Clients can pass a close callback to be notified when
    *        the panel successfully closes.
    */
   close: function DP_close(aCloseCallback) {
-    if (!this._win) {
+    if (!this.globalUI) {
       return;
     }
-    delete this._win._scriptDebugger;
+    delete this.globalUI._scriptDebugger;
     this._win = null;
     this._tab = null;
 
     DebuggerPreferences.height = this._frame.height;
     this._frame.removeEventListener("Debugger:Close", this.close, true);
     this._frame.removeEventListener("unload", this.close, true);
 
     // This method is also used as an event handler, so only
@@ -294,17 +318,18 @@ DebuggerPane.prototype = {
 
     this._nbox.removeChild(this._splitter);
     this._nbox.removeChild(this._frame);
 
     this._splitter = null;
     this._frame = null;
     this._nbox = null;
 
-    this._globalUI.refreshCommand();
+    this.globalUI.refreshCommand();
+    this.globalUI = null;
   },
 
   /**
    * Gets the tab owning this debugger instance.
    * @return XULElement
    */
   get ownerTab() {
     return this._tab;
@@ -333,31 +358,31 @@ DebuggerPane.prototype = {
 
 /**
  * Creates a window that will host a remote debugger.
  *
  * @param DebuggerUI aDebuggerUI
  *        The parent instance creating the new debugger.
  */
 function RemoteDebuggerWindow(aDebuggerUI) {
-  this._globalUI = aDebuggerUI;
+  this.globalUI = aDebuggerUI;
   this._win = aDebuggerUI.chromeWindow;
 
   this._create();
 }
 
 RemoteDebuggerWindow.prototype = {
 
   /**
    * Creates and initializes the widgets containing the remote debugger UI.
    */
   _create: function DP__create() {
-    this._win._remoteDebugger = this;
+    this.globalUI._remoteDebugger = this;
 
-    this._dbgwin = this._globalUI.chromeWindow.open(DBG_XUL,
+    this._dbgwin = this.globalUI.chromeWindow.open(DBG_XUL,
       L10N.getStr("remoteDebuggerWindowTitle"),
       "width=" + DebuggerPreferences.remoteWinWidth + "," +
       "height=" + DebuggerPreferences.remoteWinHeight + "," +
       "chrome,dependent,resizable,centerscreen");
 
     this._dbgwin._remoteFlag = true;
 
     this.close = this.close.bind(this);
@@ -375,20 +400,21 @@ RemoteDebuggerWindow.prototype = {
       self.getBreakpoint = bkp.getBreakpoint;
     }, true);
   },
 
   /**
    * Closes the remote debugger, along with the parent window if necessary.
    */
   close: function DP_close() {
-    if (!this._win) {
+    if (!this.globalUI) {
       return;
     }
-    delete this._win._remoteDebugger;
+    delete this.globalUI._remoteDebugger;
+    this.globalUI = null;
     this._win = null;
 
     this._dbgwin.close();
     this._dbgwin = null;
   },
 
   /**
    * Gets the remote debugger content window.
@@ -409,25 +435,26 @@ RemoteDebuggerWindow.prototype = {
     }
     return null;
   }
 };
 
 /**
  * Creates a process that will hold a chrome debugger.
  *
+ * @param DebuggerUI aDebuggerUI
+ *        The parent instance creating the new debugger.
  * @param function aOnClose
  *        Optional, a function called when the process exits.
  * @param function aOnRun
  *        Optional, a function called when the process starts running.
- * @param nsIDOMWindow aWindow
- *        The chrome window for which the debugger instance is created.
  */
-function ChromeDebuggerProcess(aWindow, aOnClose, aOnRun) {
-  this._win = aWindow;
+function ChromeDebuggerProcess(aDebuggerUI, aOnClose, aOnRun) {
+  this.globalUI = aDebuggerUI;
+  this._win = aDebuggerUI.chromeWindow;
   this._closeCallback = aOnClose;
   this._runCallback = aOnRun;
 
   this._initServer();
   this._initProfile();
   this._create();
 }
 
@@ -489,17 +516,17 @@ ChromeDebuggerProcess.prototype = {
     this._dbgProfile = profileService.createProfile(null, null, dbgProfileName);
     profileService.flush();
   },
 
   /**
    * Creates and initializes the profile & process for the remote debugger.
    */
   _create: function RDP__create() {
-    this._win._chromeDebugger = this;
+    this.globalUI._chromeDebugger = this;
 
     let file = FileUtils.getFile("CurProcD",
       [Services.appinfo.OS == "WINNT" ? "firefox.exe"
                                       : "firefox-bin"]);
 
     let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
     process.init(file);
 
@@ -516,20 +543,21 @@ ChromeDebuggerProcess.prototype = {
       this._runCallback.call({}, this);
     }
   },
 
   /**
    * Closes the remote debugger, removing the profile and killing the process.
    */
   close: function RDP_close() {
-    if (!this._win) {
+    if (!this.globalUI) {
       return;
     }
-    delete this._win._chromeDebugger;
+    delete this.globalUI._chromeDebugger;
+    this.globalUI = null;
     this._win = null;
 
     if (this._dbgProcess.isRunning) {
       this._dbgProcess.kill();
     }
     if (this._dbgProfile) {
       this._dbgProfile.remove(false);
     }
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -10,16 +10,17 @@ relativesrcdir  = browser/devtools/debug
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
 	browser_dbg_createRemote.js \
 	browser_dbg_createChrome.js \
 	browser_dbg_debugger-tab-switch.js \
+	browser_dbg_debugger-tab-switch-window.js \
 	browser_dbg_debuggerstatement.js \
 	browser_dbg_listtabs.js \
 	browser_dbg_tabactor-01.js \
 	browser_dbg_tabactor-02.js \
 	browser_dbg_contextactor-01.js \
 	browser_dbg_contextactor-02.js \
 	testactors.js \
 	browser_dbg_nav-01.js \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_debugger-tab-switch-window.js
@@ -0,0 +1,244 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+let gInitialTab, gTab1, gTab2, gTab3, gTab4;
+let gInitialWindow, gSecondWindow;
+let gPane1, gPane2;
+let gNbox;
+
+/**
+ * Tests that a debugger instance can't be opened in multiple windows at once,
+ * and that on such an attempt a notification is shown, which can either switch
+ * to the old debugger instance, or close the old instance to open a new one.
+ */
+
+function test() {
+  gInitialWindow = window;
+  gInitialTab = window.gBrowser.selectedTab;
+  gNbox = gInitialWindow.gBrowser.getNotificationBox(gInitialWindow.gBrowser.selectedBrowser);
+
+  testTab1_initialWindow(function() {
+    testTab2_secondWindow(function() {
+      testTab3_secondWindow(function() {
+        testTab4_secondWindow(function() {
+          lastTest(function() {
+            cleanup(function() {
+              finish();
+            });
+          });
+        });
+      });
+    });
+  });
+}
+
+function testTab1_initialWindow(callback) {
+  gTab1 = addTab(TAB1_URL, function() {
+    gInitialWindow.gBrowser.selectedTab = gTab1;
+    gNbox = gInitialWindow.gBrowser.getNotificationBox(gInitialWindow.gBrowser.selectedBrowser);
+
+    is(gNbox.getNotificationWithValue("debugger-tab-switch"), null,
+      "Shouldn't have a tab switch notification.");
+    ok(!gInitialWindow.DebuggerUI.getDebugger(),
+      "Shouldn't have a debugger pane for this tab yet.");
+
+    info("Toggling a debugger (1).");
+
+    gPane1 = gInitialWindow.DebuggerUI.toggleDebugger();
+    ok(gPane1, "toggleDebugger() should return a pane.");
+    is(gPane1.ownerTab, gTab1, "Incorrect tab owner.");
+
+    is(gInitialWindow.DebuggerUI.getDebugger(), gPane1,
+      "getDebugger() should return the same pane as toggleDebugger().");
+
+    wait_for_connect_and_resume(function dbgLoaded() {
+      info("First debugger has finished loading correctly.");
+      executeSoon(function() {
+        callback();
+      });
+    }, gInitialWindow);
+  }, gInitialWindow);
+}
+
+function testTab2_secondWindow(callback) {
+  gSecondWindow = addWindow();
+
+  gTab2 = addTab(TAB1_URL, function() {
+    gSecondWindow.gBrowser.selectedTab = gTab2;
+    gNbox = gSecondWindow.gBrowser.getNotificationBox(gSecondWindow.gBrowser.selectedBrowser);
+
+    is(gNbox.getNotificationWithValue("debugger-tab-switch"), null,
+      "Shouldn't have a tab switch notification yet.");
+    ok(gSecondWindow.DebuggerUI.findDebugger(),
+      "Should already have a debugger pane for another tab.");
+
+    gNbox.addEventListener("AlertActive", function active() {
+      gNbox.removeEventListener("AlertActive", active, true);
+      executeSoon(function() {
+        ok(gPane2, "toggleDebugger() should always return a pane.");
+        is(gPane2.ownerTab, gTab1, "Incorrect tab owner.");
+
+        is(gSecondWindow.DebuggerUI.findDebugger(), gPane1,
+          "findDebugger() should return the same pane as the first call to toggleDebugger().");
+        is(gSecondWindow.DebuggerUI.findDebugger(), gPane2,
+          "findDebugger() should return the same pane as the second call to toggleDebugger().");
+
+        info("Second debugger has not loaded.");
+
+        let notification = gNbox.getNotificationWithValue("debugger-tab-switch");
+        ok(gNbox.currentNotification, "Should have a tab switch notification.");
+        is(gNbox.currentNotification, notification, "Incorrect current notification.");
+
+        info("Notification will be simply closed.");
+        notification.close();
+
+        executeSoon(function() {
+          callback();
+        });
+      });
+    }, true);
+
+    info("Toggling a debugger (2).");
+
+    gPane2 = gSecondWindow.DebuggerUI.toggleDebugger();
+  }, gSecondWindow);
+}
+
+function testTab3_secondWindow(callback) {
+  gTab3 = addTab(TAB1_URL, function() {
+    gSecondWindow.gBrowser.selectedTab = gTab3;
+    gNbox = gSecondWindow.gBrowser.getNotificationBox(gSecondWindow.gBrowser.selectedBrowser);
+
+    is(gNbox.getNotificationWithValue("debugger-tab-switch"), null,
+      "Shouldn't have a tab switch notification.");
+    ok(gSecondWindow.DebuggerUI.findDebugger(),
+      "Should already have a debugger pane for another tab.");
+
+    gNbox.addEventListener("AlertActive", function active() {
+      gNbox.removeEventListener("AlertActive", active, true);
+      executeSoon(function() {
+        ok(gPane2, "toggleDebugger() should always return a pane.");
+        is(gPane2.ownerTab, gTab1, "Incorrect tab owner.");
+
+        is(gSecondWindow.DebuggerUI.findDebugger(), gPane1,
+          "findDebugger() should return the same pane as the first call to toggleDebugger().");
+        is(gSecondWindow.DebuggerUI.findDebugger(), gPane2,
+          "findDebugger() should return the same pane as the second call to toggleDebugger().");
+
+        info("Second debugger has not loaded.");
+
+        let notification = gNbox.getNotificationWithValue("debugger-tab-switch");
+        ok(gNbox.currentNotification, "Should have a tab switch notification.");
+        is(gNbox.currentNotification, notification, "Incorrect current notification.");
+
+        gInitialWindow.gBrowser.selectedTab = gInitialTab;
+        gInitialWindow.gBrowser.tabContainer.addEventListener("TabSelect", function tabSelect() {
+          gInitialWindow.gBrowser.tabContainer.removeEventListener("TabSelect", tabSelect, true);
+          executeSoon(function() {
+            callback();
+          });
+        }, true);
+
+        let buttonSwitch = notification.querySelectorAll("button")[0];
+        buttonSwitch.focus();
+        EventUtils.sendKey("SPACE", gSecondWindow);
+        info("The switch button on the notification was pressed.");
+      });
+    }, true);
+
+    info("Toggling a debugger (3).");
+
+    gPane2 = gSecondWindow.DebuggerUI.toggleDebugger();
+  }, gSecondWindow);
+}
+
+function testTab4_secondWindow(callback) {
+  is(gInitialWindow.gBrowser.selectedTab, gTab1,
+    "Should've switched to the first debugged tab.");
+
+  gTab4 = addTab(TAB1_URL, function() {
+    gSecondWindow.gBrowser.selectedTab = gTab4;
+    gNbox = gSecondWindow.gBrowser.getNotificationBox(gSecondWindow.gBrowser.selectedBrowser);
+
+    is(gNbox.getNotificationWithValue("debugger-tab-switch"), null,
+      "Shouldn't have a tab switch notification.");
+    ok(gSecondWindow.DebuggerUI.findDebugger(),
+      "Should already have a debugger pane for another tab.");
+
+    gNbox.addEventListener("AlertActive", function active() {
+      gNbox.removeEventListener("AlertActive", active, true);
+      executeSoon(function() {
+        ok(gPane2, "toggleDebugger() should always return a pane.");
+        is(gPane2.ownerTab, gTab1, "Incorrect tab owner.");
+
+        is(gSecondWindow.DebuggerUI.findDebugger(), gPane1,
+          "findDebugger() should return the same pane as the first call to toggleDebugger().");
+        is(gSecondWindow.DebuggerUI.findDebugger(), gPane2,
+          "findDebugger() should return the same pane as the second call to toggleDebugger().");
+
+        info("Second debugger has not loaded.");
+
+        let notification = gNbox.getNotificationWithValue("debugger-tab-switch");
+        ok(gNbox.currentNotification, "Should have a tab switch notification.");
+        is(gNbox.currentNotification, notification, "Incorrect current notification.");
+
+        let buttonOpen = notification.querySelectorAll("button")[1];
+        buttonOpen.focus();
+        EventUtils.sendKey("SPACE", gSecondWindow);
+        info("The open button on the notification was pressed.");
+
+        wait_for_connect_and_resume(function() {
+          callback();
+        }, gSecondWindow);
+      });
+    }, true);
+
+    info("Toggling a debugger (4).");
+
+    gPane2 = gSecondWindow.DebuggerUI.toggleDebugger();
+  }, gSecondWindow);
+}
+
+function lastTest(callback) {
+  is(gInitialWindow.gBrowser.selectedTab, gTab1,
+    "The initial window should continue having selected the first debugged tab.");
+  is(gSecondWindow.gBrowser.selectedTab, gTab4,
+    "Should currently be in the fourth tab.");
+  is(gSecondWindow.DebuggerUI.findDebugger().ownerTab, gTab4,
+    "The debugger should be open for the fourth tab.");
+
+  is(gNbox.getNotificationWithValue("debugger-tab-switch"), null,
+    "Shouldn't have a tab switch notification.");
+
+  info("Second debugger has loaded.");
+
+  executeSoon(function() {
+    callback();
+  });
+}
+
+function cleanup(callback)
+{
+  gPane1 = null;
+  gPane2 = null;
+  gNbox = null;
+
+  closeDebuggerAndFinish(false, function() {
+    removeTab(gTab1, gInitialWindow);
+    removeTab(gTab2, gSecondWindow);
+    removeTab(gTab3, gSecondWindow);
+    removeTab(gTab4, gSecondWindow);
+    gSecondWindow.close();
+    gTab1 = null;
+    gTab2 = null;
+    gTab3 = null;
+    gTab4 = null;
+    gInitialWindow = null;
+    gSecondWindow = null;
+
+    callback();
+  }, gSecondWindow);
+}
--- a/browser/devtools/debugger/test/browser_dbg_debugger-tab-switch.js
+++ b/browser/devtools/debugger/test/browser_dbg_debugger-tab-switch.js
@@ -3,16 +3,22 @@
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 let gTab1, gTab2, gTab3, gTab4;
 let gPane1, gPane2;
 let gNbox;
 
+/**
+ * Tests that a debugger instance can't be opened in multiple tabs at once,
+ * and that on such an attempt a notification is shown, which can either switch
+ * to the old debugger instance, or close the old instance to open a new one.
+ */
+
 function test() {
   gNbox = gBrowser.getNotificationBox(gBrowser.selectedBrowser);
 
   testTab1(function() {
     testTab2(function() {
       testTab3(function() {
         testTab4(function() {
           lastTest(function() {
@@ -45,17 +51,17 @@ function testTab1(callback) {
     is(DebuggerUI.getDebugger(), gPane1,
       "getDebugger() should return the same pane as toggleDebugger().");
 
     wait_for_connect_and_resume(function dbgLoaded() {
       info("First debugger has finished loading correctly.");
       executeSoon(function() {
         callback();
       });
-    }, true);
+    });
   });
 }
 
 function testTab2(callback) {
   gTab2 = addTab(TAB1_URL, function() {
     gBrowser.selectedTab = gTab2;
     gNbox = gBrowser.getNotificationBox(gBrowser.selectedBrowser);
 
@@ -203,18 +209,18 @@ function lastTest(callback) {
 
   info("Second debugger has loaded.");
 
   executeSoon(function() {
     callback();
   });
 }
 
-function cleanup(callback) {
-
+function cleanup(callback)
+{
   gPane1 = null;
   gPane2 = null;
   gNbox = null;
 
   closeDebuggerAndFinish(false, function() {
     removeTab(gTab1);
     removeTab(gTab2);
     removeTab(gTab3);
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -28,63 +28,84 @@ registerCleanupFunction(function() {
 
 if (!DebuggerServer.initialized) {
   DebuggerServer.init(function () { return true; });
   DebuggerServer.addBrowserActors();
 }
 
 waitForExplicitFinish();
 
-function addTab(aURL, aOnload)
+function addWindow()
 {
-  gBrowser.selectedTab = gBrowser.addTab(aURL);
+  let windowReference = window.open();
+  let chromeWindow = windowReference
+    .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+    .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
+    .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+
+  return chromeWindow;
+}
 
-  let tab = gBrowser.selectedTab;
+function addTab(aURL, aOnload, aWindow)
+{
+  let targetWindow = aWindow || window;
+  let targetBrowser = targetWindow.gBrowser;
+
+  targetWindow.focus();
+  targetBrowser.selectedTab = targetBrowser.addTab(aURL);
+
+  let tab = targetBrowser.selectedTab;
   if (aOnload) {
     let handler = function() {
       if (tab.linkedBrowser.currentURI.spec == aURL) {
         tab.removeEventListener("load", handler, false);
         aOnload();
       }
     }
     tab.addEventListener("load", handler, false);
   }
 
   return tab;
 }
 
-function removeTab(aTab) {
-  gBrowser.removeTab(aTab);
+function removeTab(aTab, aWindow) {
+  let targetWindow = aWindow || window;
+  let targetBrowser = targetWindow.gBrowser;
+
+  targetBrowser.removeTab(aTab);
 }
 
-function closeDebuggerAndFinish(aRemoteFlag, aCallback) {
+function closeDebuggerAndFinish(aRemoteFlag, aCallback, aWindow) {
+  let targetWindow = aWindow || window;
+  let debuggerUI = targetWindow.DebuggerUI;
+
   let debuggerClosed = false;
   let debuggerDisconnected = false;
 
   function _maybeFinish() {
     if (debuggerClosed && debuggerDisconnected) {
       if (!aCallback)
         aCallback = finish;
       aCallback();
     }
   }
 
-  DebuggerUI.chromeWindow.addEventListener("Debugger:Shutdown", function cleanup() {
-    DebuggerUI.chromeWindow.removeEventListener("Debugger:Shutdown", cleanup, false);
+  debuggerUI.chromeWindow.addEventListener("Debugger:Shutdown", function cleanup() {
+    debuggerUI.chromeWindow.removeEventListener("Debugger:Shutdown", cleanup, false);
     debuggerDisconnected = true;
     _maybeFinish();
   }, false);
   if (!aRemoteFlag) {
-    DebuggerUI.getDebugger().close(function() {
+    debuggerUI.getDebugger().close(function() {
       debuggerClosed = true;
       _maybeFinish();
     });
   } else {
     debuggerClosed = true;
-    DebuggerUI.getRemoteDebugger().close();
+    debuggerUI.getRemoteDebugger().close();
   }
 }
 
 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);
@@ -127,20 +148,23 @@ function debug_tab_pane(aURL, aOnDebuggi
       // Wait for the initial resume...
       pane.contentWindow.gClient.addOneTimeListener("resumed", function() {
         aOnDebugging(tab, debuggee, pane);
       });
     }, true);
   });
 }
 
-function wait_for_connect_and_resume(aOnDebugging)
+function wait_for_connect_and_resume(aOnDebugging, aWindow)
 {
-  window.document.addEventListener("Debugger:Connecting", function dbgConnected(aEvent) {
-    window.document.removeEventListener("Debugger:Connecting", dbgConnected, true);
+  let targetWindow = aWindow || window;
+  let targetDocument = targetWindow.document;
+
+  targetDocument.addEventListener("Debugger:Connecting", function dbgConnected(aEvent) {
+    targetDocument.removeEventListener("Debugger:Connecting", dbgConnected, true);
 
     // Wait for the initial resume...
     aEvent.target.ownerDocument.defaultView.gClient.addOneTimeListener("resumed", function() {
       aOnDebugging();
     });
   }, true);
 }