Bug 818134 - Allow multiple debuggers in toolboxes to debug separate tabs; r=rcampbell,dcamp
authorPanos Astithas <past@mozilla.com>
Tue, 09 Apr 2013 14:17:03 +0300
changeset 128122 5306afe4579bcef1c554cd50248da8f8bab5ad48
parent 128121 26fb3bd67f5f613835504058d6708cb2440d592e
child 128123 9d5f05a6d497f968fd1580f6af8f84486323b0c3
push id24521
push userryanvm@gmail.com
push dateTue, 09 Apr 2013 18:31:04 +0000
treeherdermozilla-central@9d5f05a6d497 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcampbell, dcamp
bugs818134
milestone23.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 818134 - Allow multiple debuggers in toolboxes to debug separate tabs; r=rcampbell,dcamp
browser/devtools/debugger/DebuggerPanel.jsm
browser/devtools/debugger/DebuggerUI.jsm
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/debugger-toolbar.js
browser/devtools/debugger/debugger.xul
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/locales/en-US/chrome/browser/devtools/debugger.properties
browser/themes/linux/devtools/debugger.css
browser/themes/osx/devtools/debugger.css
browser/themes/windows/devtools/debugger.css
toolkit/devtools/debugger/nsIJSInspector.idl
toolkit/devtools/debugger/nsJSInspector.cpp
toolkit/devtools/debugger/nsJSInspector.h
toolkit/devtools/debugger/server/dbg-script-actors.js
toolkit/devtools/debugger/tests/unit/test_nsjsinspector.js
--- a/browser/devtools/debugger/DebuggerPanel.jsm
+++ b/browser/devtools/debugger/DebuggerPanel.jsm
@@ -81,15 +81,9 @@ DebuggerPanel.prototype = {
 
   getBreakpoint: function() {
     return this._bkp.getBreakpoint.apply(this._bkp, arguments);
   },
 
   getAllBreakpoints: function() {
     return this._bkp.store;
   },
-
-  // Private
-
-  _ensureOnlyOneRunningDebugger: function() {
-    // FIXME
-  },
 };
--- a/browser/devtools/debugger/DebuggerUI.jsm
+++ b/browser/devtools/debugger/DebuggerUI.jsm
@@ -7,17 +7,16 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const DBG_XUL = "chrome://browser/content/debugger.xul";
 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
 const CHROME_DEBUGGER_PROFILE_NAME = "-chrome-debugger";
-const TAB_SWITCH_NOTIFICATION = "debugger-tab-switch";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this,
   "DebuggerServer", "resource://gre/modules/devtools/dbg-server.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this,
   "Services", "resource://gre/modules/Services.jsm");
@@ -78,20 +77,16 @@ DebuggerUI.prototype = {
    * @return DebuggerPane | null
    *         The script debugger instance if it's started, null if stopped.
    */
   toggleDebugger: function DUI_toggleDebugger() {
     let scriptDebugger = this.findDebugger();
     let selectedTab = this.chromeWindow.gBrowser.selectedTab;
 
     if (scriptDebugger) {
-      if (scriptDebugger.ownerTab !== selectedTab) {
-        this.showTabSwitchNotification();
-        return scriptDebugger;
-      }
       scriptDebugger.close();
       return null;
     }
     return new DebuggerPane(this, selectedTab);
   },
 
   /**
    * Starts a remote debugger in a new window, or stops it if already started.
@@ -166,73 +161,16 @@ DebuggerUI.prototype = {
   /**
    * Get the chrome debugger for the current firefox instance.
    *
    * @return ChromeDebuggerProcess | null
    *         The chrome debugger instance if it exists, null otherwise.
    */
   getChromeDebugger: function DUI_getChromeDebugger() {
     return '_chromeDebugger' in this ? this._chromeDebugger : null;
-  },
-
-  /**
-   * Currently, there can only be one debugger per tab.
-   * Show an asynchronous notification which asks the user to switch the
-   * script debugger to the current tab if it's already open in another one.
-   */
-  showTabSwitchNotification: function DUI_showTabSwitchNotification() {
-    let gBrowser = this.chromeWindow.gBrowser;
-    let selectedBrowser = gBrowser.selectedBrowser;
-
-    let nbox = gBrowser.getNotificationBox(selectedBrowser);
-    let notification = nbox.getNotificationWithValue(TAB_SWITCH_NOTIFICATION);
-    if (notification) {
-      nbox.removeNotification(notification);
-      return;
-    }
-    let self = this;
-
-    let buttons = [{
-      id: "debugger.confirmTabSwitch.buttonSwitch",
-      label: L10N.getStr("confirmTabSwitch.buttonSwitch"),
-      accessKey: L10N.getStr("confirmTabSwitch.buttonSwitch.accessKey"),
-      callback: function DUI_notificationButtonSwitch() {
-        let scriptDebugger = self.findDebugger();
-        let targetWindow = scriptDebugger.globalUI.chromeWindow;
-        targetWindow.gBrowser.selectedTab = scriptDebugger.ownerTab;
-        targetWindow.focus();
-      }
-    }, {
-      id: "debugger.confirmTabSwitch.buttonOpen",
-      label: L10N.getStr("confirmTabSwitch.buttonOpen"),
-      accessKey: L10N.getStr("confirmTabSwitch.buttonOpen.accessKey"),
-      callback: function DUI_notificationButtonOpen() {
-        let scriptDebugger = self.findDebugger();
-        let targetWindow = scriptDebugger.globalUI.chromeWindow;
-        scriptDebugger.close();
-
-        targetWindow.addEventListener("Debugger:Shutdown", function onShutdown() {
-          targetWindow.removeEventListener("Debugger:Shutdown", onShutdown, false);
-          Services.tm.currentThread.dispatch({ run: function() {
-            self.toggleDebugger();
-          }}, 0);
-        }, false);
-      }
-    }];
-
-    let message = L10N.getStr("confirmTabSwitch.message");
-    let imageURL = "chrome://browser/skin/Info.png";
-
-    notification = nbox.appendNotification(
-      message, TAB_SWITCH_NOTIFICATION,
-      imageURL, nbox.PRIORITY_WARNING_HIGH, buttons, null);
-
-    // Make sure this is not a transient notification, to avoid the automatic
-    // transient notification removal.
-    notification.persistence = -1;
   }
 };
 
 /**
  * Creates a pane that will host the debugger.
  *
  * @param DebuggerUI aDebuggerUI
  *        The parent instance creating the new debugger.
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -251,26 +251,35 @@ let DebuggerController = {
           Cu.reportError("Couldn't attach to thread: " + aResponse.error);
           return;
         }
         this.activeThread = aThreadClient;
 
         this.ThreadState.connect();
         this.StackFrames.connect();
         this.SourceScripts.connect();
-        aThreadClient.resume();
+        aThreadClient.resume(this._ensureResumptionOrder);
 
         if (aCallback) {
           aCallback();
         }
       });
     });
   },
 
   /**
+   * Warn if resuming execution produced a wrongOrder error.
+   */
+  _ensureResumptionOrder: function DC__ensureResumptionOrder(aResponse) {
+    if (aResponse.error == "wrongOrder") {
+      DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl);
+    }
+  },
+
+  /**
    * Sets up a chrome debugging session.
    *
    * @param DebuggerClient aClient
    *        The debugger client.
    * @param object aChromeDebugger
    *        The remote protocol grip of the chrome debugger.
    * @param function aCallback
    *        A function to invoke once the client attached to the active thread.
@@ -287,17 +296,17 @@ let DebuggerController = {
         Cu.reportError("Couldn't attach to thread: " + aResponse.error);
         return;
       }
       this.activeThread = aThreadClient;
 
       this.ThreadState.connect();
       this.StackFrames.connect();
       this.SourceScripts.connect();
-      aThreadClient.resume();
+      aThreadClient.resume(this._ensureResumptionOrder);
 
       if (aCallback) {
         aCallback();
       }
     });
   },
 
   /**
@@ -512,17 +521,17 @@ StackFrames.prototype = {
       }
     }
     // Got our evaluation of the current breakpoint's conditional expression.
     if (this._isConditionalBreakpointEvaluation) {
       this._isConditionalBreakpointEvaluation = false;
       // If the breakpoint's conditional expression evaluation is falsy,
       // automatically resume execution.
       if (VariablesView.isFalsy({ value: this.currentEvaluation.return })) {
-        this.activeThread.resume();
+        this.activeThread.resume(DebuggerController._ensureResumptionOrder);
         return;
       }
     }
 
 
     // Watch expressions are evaluated in the context of the topmost frame,
     // and the results and displayed in the variables view.
     if (this.currentWatchExpressions) {
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -22,16 +22,17 @@ function ToolbarView() {
 ToolbarView.prototype = {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function DVT_initialize() {
     dumpn("Initializing the ToolbarView");
 
     this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle");
+    this._resumeOrderPanel = document.getElementById("resumption-order-panel");
     this._resumeButton = document.getElementById("resume");
     this._stepOverButton = document.getElementById("step-over");
     this._stepInButton = document.getElementById("step-in");
     this._stepOutButton = document.getElementById("step-out");
     this._chromeGlobals = document.getElementById("chrome-globals");
 
     let resumeKey = LayoutHelpers.prettyKey(document.getElementById("resumeKey"), true);
     let stepOverKey = LayoutHelpers.prettyKey(document.getElementById("stepOverKey"), true);
@@ -85,16 +86,29 @@ ToolbarView.prototype = {
     // If we're attached, do the opposite.
     else if (aState == "attached") {
       this._resumeButton.removeAttribute("checked");
       this._resumeButton.setAttribute("tooltiptext", this._pauseTooltip);
     }
   },
 
   /**
+   * Display a warning when trying to resume a debuggee while another is paused.
+   * Debuggees must be unpaused in a Last-In-First-Out order.
+   *
+   * @param string aPausedUrl
+   *        The URL of the last paused debuggee.
+   */
+  showResumeWarning: function DVT_showResumeWarning(aPausedUrl) {
+    let label = L10N.getFormatStr("resumptionOrderPanelTitle", [aPausedUrl]);
+    document.getElementById("resumption-panel-desc").textContent = label;
+    this._resumeOrderPanel.openPopup(this._resumeButton);
+  },
+
+  /**
    * Sets the chrome globals container hidden or visible. It's hidden by default.
    *
    * @param boolean aVisibleFlag
    *        Specifies the intended visibility.
    */
   toggleChromeGlobalsContainer: function DVT_toggleChromeGlobalsContainer(aVisibleFlag) {
     this._chromeGlobals.setAttribute("hidden", !aVisibleFlag);
   },
@@ -110,17 +124,18 @@ ToolbarView.prototype = {
     });
   },
 
   /**
    * Listener handling the pause/resume button click event.
    */
   _onResumePressed: function DVT__onResumePressed() {
     if (DebuggerController.activeThread.paused) {
-      DebuggerController.activeThread.resume();
+      let warn = DebuggerController._ensureResumptionOrder;
+      DebuggerController.activeThread.resume(warn);
     } else {
       DebuggerController.activeThread.interrupt();
     }
   },
 
   /**
    * Listener handling the step over button click event.
    */
@@ -144,16 +159,17 @@ ToolbarView.prototype = {
    */
   _onStepOutPressed: function DVT__onStepOutPressed() {
     if (DebuggerController.activeThread.paused) {
       DebuggerController.activeThread.stepOut();
     }
   },
 
   _instrumentsPaneToggleButton: null,
+  _resumeOrderPanel: null,
   _resumeButton: null,
   _stepOverButton: null,
   _stepInButton: null,
   _stepOutButton: null,
   _chromeGlobals: null,
   _resumeTooltip: "",
   _pauseTooltip: "",
   _stepOverTooltip: "",
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -335,9 +335,19 @@
          noautofocus="true">
     <vbox>
       <label id="conditional-breakpoint-panel-description"
              value="&debuggerUI.condBreakPanelTitle;"/>
       <textbox id="conditional-breakpoint-panel-textbox"/>
     </vbox>
   </panel>
 
+  <panel id="resumption-order-panel"
+         type="arrow"
+         noautofocus="true"
+         position="before_start">
+    <hbox align="start">
+      <image class="alert-icon"/>
+      <label id="resumption-panel-desc" class="description"/>
+    </hbox>
+  </panel>
+
 </window>
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -12,18 +12,16 @@ include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_aaa_run_first_leaktest.js \
 	browser_dbg_clean-exit.js \
 	browser_dbg_cmd.js \
 	$(browser_dbg_cmd_break.js disabled until bug 722727 is fixed) \
 	browser_dbg_createChrome.js \
 	$(browser_dbg_createRemote.js disabled for intermittent failures, bug 753225) \
-	$(browser_dbg_debugger-tab-switch.js disabled until issues 106, 40 are fixed) \
-	$(browser_dbg_debugger-tab-switch-window.js disabled until issues 106, 40 are fixed) \
 	browser_dbg_debuggerstatement.js \
 	browser_dbg_listtabs.js \
 	browser_dbg_tabactor-01.js \
 	browser_dbg_tabactor-02.js \
 	browser_dbg_globalactor-01.js \
 	browser_dbg_nav-01.js \
 	browser_dbg_propertyview-01.js \
 	browser_dbg_propertyview-02.js \
deleted file mode 100644
--- a/browser/devtools/debugger/test/browser_dbg_debugger-tab-switch-window.js
+++ /dev/null
@@ -1,244 +0,0 @@
-/* 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);
-}
deleted file mode 100644
--- a/browser/devtools/debugger/test/browser_dbg_debugger-tab-switch.js
+++ /dev/null
@@ -1,235 +0,0 @@
-/* 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 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() {
-            cleanup(function() {
-              finish();
-            });
-          });
-        });
-      });
-    });
-  });
-}
-
-function testTab1(callback) {
-  gTab1 = addTab(TAB1_URL, function() {
-    gBrowser.selectedTab = gTab1;
-    gNbox = gBrowser.getNotificationBox(gBrowser.selectedBrowser);
-
-    is(gNbox.getNotificationWithValue("debugger-tab-switch"), null,
-      "Shouldn't have a tab switch notification.");
-    ok(!DebuggerUI.getDebugger(),
-      "Shouldn't have a debugger pane for this tab yet.");
-
-    info("Toggling a debugger (1).");
-
-    gPane1 = DebuggerUI.toggleDebugger();
-    ok(gPane1, "toggleDebugger() should return a pane.");
-    is(gPane1.ownerTab, gTab1, "Incorrect tab owner.");
-
-    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();
-      });
-    });
-  });
-}
-
-function testTab2(callback) {
-  gTab2 = addTab(TAB1_URL, function() {
-    gBrowser.selectedTab = gTab2;
-    gNbox = gBrowser.getNotificationBox(gBrowser.selectedBrowser);
-
-    is(gNbox.getNotificationWithValue("debugger-tab-switch"), null,
-      "Shouldn't have a tab switch notification yet.");
-    ok(DebuggerUI.getDebugger(),
-      "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(DebuggerUI.getDebugger(), gPane1,
-          "getDebugger() should return the same pane as the first call to toggleDebugger().");
-        is(DebuggerUI.getDebugger(), gPane2,
-          "getDebugger() 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 = DebuggerUI.toggleDebugger();
-  });
-}
-
-function testTab3(callback) {
-  gTab3 = addTab(TAB1_URL, function() {
-    gBrowser.selectedTab = gTab3;
-    gNbox = gBrowser.getNotificationBox(gBrowser.selectedBrowser);
-
-    is(gNbox.getNotificationWithValue("debugger-tab-switch"), null,
-      "Shouldn't have a tab switch notification.");
-    ok(DebuggerUI.getDebugger(),
-      "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(DebuggerUI.getDebugger(), gPane1,
-          "getDebugger() should return the same pane as the first call to toggleDebugger().");
-        is(DebuggerUI.getDebugger(), gPane2,
-          "getDebugger() 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.");
-
-        gBrowser.tabContainer.addEventListener("TabSelect", function tabSelect() {
-          gBrowser.tabContainer.removeEventListener("TabSelect", tabSelect, true);
-          executeSoon(function() {
-            callback();
-          });
-        }, true);
-
-        let buttonSwitch = notification.querySelectorAll("button")[0];
-        buttonSwitch.focus();
-        EventUtils.sendKey("SPACE");
-        info("The switch button on the notification was pressed.");
-      });
-    }, true);
-
-    info("Toggling a debugger (3).");
-
-    gPane2 = DebuggerUI.toggleDebugger();
-  });
-}
-
-function testTab4(callback) {
-  is(gBrowser.selectedTab, gTab1,
-    "Should've switched to the first debugged tab.");
-
-  gTab4 = addTab(TAB1_URL, function() {
-    gBrowser.selectedTab = gTab4;
-    gNbox = gBrowser.getNotificationBox(gBrowser.selectedBrowser);
-
-    is(gNbox.getNotificationWithValue("debugger-tab-switch"), null,
-      "Shouldn't have a tab switch notification.");
-    ok(DebuggerUI.getDebugger(),
-      "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(DebuggerUI.getDebugger(), gPane1,
-          "getDebugger() should return the same pane as the first call to toggleDebugger().");
-        is(DebuggerUI.getDebugger(), gPane2,
-          "getDebugger() 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");
-        info("The open button on the notification was pressed.");
-
-        wait_for_connect_and_resume(function() {
-          callback();
-        });
-      });
-    }, true);
-
-    info("Toggling a debugger (4).");
-
-    gPane2 = DebuggerUI.toggleDebugger();
-  });
-}
-
-function lastTest(callback) {
-  isnot(gBrowser.selectedTab, gTab1,
-    "Shouldn't have switched to the first debugged tab.");
-  is(gBrowser.selectedTab, gTab4,
-    "Should currently be in the fourth tab.");
-  is(DebuggerUI.getDebugger().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);
-    removeTab(gTab2);
-    removeTab(gTab3);
-    removeTab(gTab4);
-    gTab1 = null;
-    gTab2 = null;
-    gTab3 = null;
-    gTab4 = null;
-
-    callback();
-  });
-}
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties
@@ -5,25 +5,16 @@
 # LOCALIZATION NOTE These strings are used inside the Debugger
 # which is available from the Web Developer sub-menu -> 'Debugger'.
 # The correct localization of this file might be to keep it in
 # English, or another language commonly spoken among web developers.
 # 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 (confirmTabSwitch): The messages displayed for all the
-# title and buttons on the notification shown when a user attempts to open a
-# debugger in a new tab while a different tab is already being debugged.
-confirmTabSwitch.message=Debugger is already open in another tab. Continuing will close the other instance.
-confirmTabSwitch.buttonSwitch=Switch to debugged tab
-confirmTabSwitch.buttonSwitch.accessKey=S
-confirmTabSwitch.buttonOpen=Open anyway
-confirmTabSwitch.buttonOpen.accessKey=O
-
 # LOCALIZATION NOTE (open.commandkey): The key used to open the debugger in
 # combination to e.g. ctrl + shift
 open.commandkey=S
 
 # LOCALIZATION NOTE (debuggerMenu.accesskey): The access key used to open the
 # debugger.
 debuggerMenu.accesskey=D
 
@@ -210,8 +201,15 @@ variablesSeparatorLabel=:
 # LOCALIZATION NOTE (watchExpressionsSeparatorLabel): The text that is displayed
 # in the watch expressions list as a separator between the code and evaluation.
 watchExpressionsSeparatorLabel=\ →
 
 # LOCALIZATION NOTE (functionSearchSeparatorLabel): The text that is displayed
 # in the functions search panel as a separator between function's inferred name
 # and its real name (if available).
 functionSearchSeparatorLabel=←
+
+# LOCALIZATION NOTE (resumptionOrderPanelTitle): This is the text that appears
+# as a description in the notification panel popup, when multiple debuggers are
+# open in separate tabs and the user tries to resume them in the wrong order.
+# The substitution parameter is the URL of the last paused window that must be
+# resumed first.
+resumptionOrderPanelTitle=There are one or more paused debuggers. Please resume the most-recently paused debugger first at: %S
--- a/browser/themes/linux/devtools/debugger.css
+++ b/browser/themes/linux/devtools/debugger.css
@@ -244,16 +244,24 @@
 
 .dbg-results-line-contents-string[match=true][focused] {
   transition-duration: 0.1s;
   transform: scale(1.75, 1.75);
 }
 
 /* Toolbar Controls */
 
+#resumption-panel-desc {
+  width: 200px;
+}
+
+#resumption-order-panel {
+  -moz-margin-start: -8px;
+}
+
 #resume {
   list-style-image: url("chrome://browser/skin/devtools/debugger-play.png");
   -moz-image-region: rect(0px,16px,16px,0px);
 }
 
 #resume[checked] {
   -moz-image-region: rect(0px,32px,16px,16px);
 }
--- a/browser/themes/osx/devtools/debugger.css
+++ b/browser/themes/osx/devtools/debugger.css
@@ -246,16 +246,24 @@
 
 .dbg-results-line-contents-string[match=true][focused] {
   transition-duration: 0.1s;
   transform: scale(1.75, 1.75);
 }
 
 /* Toolbar Controls */
 
+#resumption-panel-desc {
+  width: 200px;
+}
+
+#resumption-order-panel {
+  -moz-margin-start: -8px;
+}
+
 #resume {
   list-style-image: url("chrome://browser/skin/devtools/debugger-play.png");
   -moz-image-region: rect(0px,16px,16px,0px);
 }
 
 #resume[checked] {
   -moz-image-region: rect(0px,32px,16px,16px);
 }
--- a/browser/themes/windows/devtools/debugger.css
+++ b/browser/themes/windows/devtools/debugger.css
@@ -244,16 +244,24 @@
 
 .dbg-results-line-contents-string[match=true][focused] {
   transition-duration: 0.1s;
   transform: scale(1.75, 1.75);
 }
 
 /* Toolbar Controls */
 
+#resumption-panel-desc {
+  width: 200px;
+}
+
+#resumption-order-panel {
+  -moz-margin-start: -8px;
+}
+
 #resume {
   list-style-image: url("chrome://browser/skin/devtools/debugger-play.png");
   -moz-image-region: rect(0px,16px,16px,0px);
 }
 
 #resume[checked] {
   -moz-image-region: rect(0px,32px,16px,16px);
 }
--- a/toolkit/devtools/debugger/nsIJSInspector.idl
+++ b/toolkit/devtools/debugger/nsIJSInspector.idl
@@ -3,32 +3,39 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 /**
  * Utilities for integrating the JSInspector object into an XPCOM
  * application.
  */
-[scriptable, uuid(dbf84113-506a-4fd3-9183-a0348c6fa9cc)]
+[scriptable, uuid(6758d0d7-e96a-4c5c-bca8-3bcbe5a15943)]
 interface nsIJSInspector : nsISupports
 {
   /**
    * Process the thread's event queue until exit.
    *
+   * @param requestor A token the requestor passes to identify the pause.
+   *
    * @return depth Returns the number of times the event loop
    *         has been nested using this API.
    */
-  unsigned long enterNestedEventLoop();
+  unsigned long enterNestedEventLoop(in jsval requestor);
 
   /**
    * Exits the current nested event loop.
    *
    * @return depth The number of nested event loops left after
    *         exiting the event loop.
    *
    * @throws NS_ERROR_FAILURE if there are no nested event loops
    *         running.
    */
   unsigned long exitNestedEventLoop();
 
   readonly attribute unsigned long eventLoopNestLevel;
+
+  /**
+   * The token provided by the actor that last requested a nested event loop.
+   */
+  readonly attribute jsval lastNestRequestor;
 };
--- a/toolkit/devtools/debugger/nsJSInspector.cpp
+++ b/toolkit/devtools/debugger/nsJSInspector.cpp
@@ -8,46 +8,53 @@
 #include "nsIJSContextStack.h"
 #include "nsThreadUtils.h"
 #include "jsapi.h"
 #include "jsfriendapi.h"
 #include "jsdbgapi.h"
 #include "mozilla/ModuleUtils.h"
 #include "nsServiceManagerUtils.h"
 #include "nsMemory.h"
+#include "nsArray.h"
+#include "nsTArray.h"
 
 #define JSINSPECTOR_CONTRACTID \
   "@mozilla.org/jsinspector;1"
 
 #define JSINSPECTOR_CID \
 { 0xec5aa99c, 0x7abb, 0x4142, { 0xac, 0x5f, 0xaa, 0xb2, 0x41, 0x9e, 0x38, 0xe2 } }
 
 namespace mozilla {
 namespace jsinspector {
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsJSInspector)
 
 NS_IMPL_ISUPPORTS1(nsJSInspector, nsIJSInspector)
 
-nsJSInspector::nsJSInspector() : mNestedLoopLevel(0)
+nsJSInspector::nsJSInspector() : mNestedLoopLevel(0), mRequestors(1), mLastRequestor(JSVAL_NULL)
 {
+  nsTArray<JS::Value> mRequestors;
 }
 
 nsJSInspector::~nsJSInspector()
 {
+  mRequestors.Clear();
 }
 
 NS_IMETHODIMP
-nsJSInspector::EnterNestedEventLoop(uint32_t *out)
+nsJSInspector::EnterNestedEventLoop(const JS::Value& requestor, uint32_t *out)
 {
   nsresult rv;
   nsCOMPtr<nsIJSContextStack> stack =
     do_GetService("@mozilla.org/js/xpc/ContextStack;1", &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  mLastRequestor = requestor;
+  mRequestors.AppendElement(requestor);
+
   uint32_t nestLevel = ++mNestedLoopLevel;
   if (NS_SUCCEEDED(stack->Push(nullptr))) {
     while (NS_SUCCEEDED(rv) && mNestedLoopLevel >= nestLevel) {
       if (!NS_ProcessNextEvent())
         rv = NS_ERROR_UNEXPECTED;
     }
 
     JSContext *cx;
@@ -55,44 +62,56 @@ nsJSInspector::EnterNestedEventLoop(uint
     NS_ASSERTION(cx == nullptr, "JSContextStack mismatch");
   } else {
     rv = NS_ERROR_FAILURE;
   }
 
   NS_ASSERTION(mNestedLoopLevel <= nestLevel,
                "nested event didn't unwind properly");
 
-  if (mNestedLoopLevel == nestLevel)
-    --mNestedLoopLevel;
+  if (mNestedLoopLevel == nestLevel) {
+    mLastRequestor = mRequestors.ElementAt(--mNestedLoopLevel);
+  }
 
   *out = mNestedLoopLevel;
   return rv;
 }
 
 NS_IMETHODIMP
 nsJSInspector::ExitNestedEventLoop(uint32_t *out)
 {
   if (mNestedLoopLevel > 0) {
-    --mNestedLoopLevel;
+    mRequestors.RemoveElementAt(--mNestedLoopLevel);
+    if (mNestedLoopLevel > 0)
+      mLastRequestor = mRequestors.ElementAt(mNestedLoopLevel - 1);
+    else
+      mLastRequestor = JSVAL_NULL;
   } else {
     return NS_ERROR_FAILURE;
   }
 
   *out = mNestedLoopLevel;
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsJSInspector::GetEventLoopNestLevel(uint32_t *out)
 {
   *out = mNestedLoopLevel;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsJSInspector::GetLastNestRequestor(JS::Value *out)
+{
+  *out = mLastRequestor;
+  return NS_OK;
+}
+
 }
 }
 
 NS_DEFINE_NAMED_CID(JSINSPECTOR_CID);
 
 static const mozilla::Module::CIDEntry kJSInspectorCIDs[] = {
   { &kJSINSPECTOR_CID, false, NULL, mozilla::jsinspector::nsJSInspectorConstructor },
   { NULL }
--- a/toolkit/devtools/debugger/nsJSInspector.h
+++ b/toolkit/devtools/debugger/nsJSInspector.h
@@ -3,30 +3,34 @@
  * 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/. */
 
 #ifndef COMPONENTS_JSINSPECTOR_H
 #define COMPONENTS_JSINSPECTOR_H
 
 #include "nsIJSInspector.h"
 #include "mozilla/Attributes.h"
+#include "nsTArray.h"
+#include "js/Value.h"
 
 namespace mozilla {
 namespace jsinspector {
 
 class nsJSInspector MOZ_FINAL : public nsIJSInspector
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIJSINSPECTOR
 
   nsJSInspector();
 
 private:
   ~nsJSInspector();
 
   uint32_t mNestedLoopLevel;
+  nsTArray<JS::Value> mRequestors;
+  JS::Value mLastRequestor;
 };
 
 }
 }
 
 #endif
--- a/toolkit/devtools/debugger/server/dbg-script-actors.js
+++ b/toolkit/devtools/debugger/server/dbg-script-actors.js
@@ -150,24 +150,24 @@ ThreadActor.prototype = {
      */
     onNewGlobal: function TA_onNewGlobal(aGlobal) {
       // Content debugging only cares about new globals in the contant window,
       // like iframe children.
       if (aGlobal.hostAnnotations &&
           aGlobal.hostAnnotations.type == "document" &&
           aGlobal.hostAnnotations.element === this.global) {
         this.addDebuggee(aGlobal);
+        // Notify the client.
+        this.conn.send({
+          from: this.actorID,
+          type: "newGlobal",
+          // TODO: after bug 801084 lands see if we need to JSONify this.
+          hostAnnotations: aGlobal.hostAnnotations
+        });
       }
-      // Notify the client.
-      this.conn.send({
-        from: this.actorID,
-        type: "newGlobal",
-        // TODO: after bug 801084 lands see if we need to JSONify this.
-        hostAnnotations: aGlobal.hostAnnotations
-      });
     }
   },
 
   disconnect: function TA_disconnect() {
     if (this._state == "paused") {
       this.onResume();
     }
 
@@ -263,16 +263,28 @@ ThreadActor.prototype = {
       return undefined;
     }
   },
 
   /**
    * Handle a protocol request to resume execution of the debuggee.
    */
   onResume: function TA_onResume(aRequest) {
+    // In case of multiple nested event loops (due to multiple debuggers open in
+    // different tabs or multiple debugger clients connected to the same tab)
+    // only allow resumption in a LIFO order.
+    if (DebuggerServer.xpcInspector.eventLoopNestLevel > 1) {
+      let lastNestRequestor = DebuggerServer.xpcInspector.lastNestRequestor;
+      if (lastNestRequestor.connection != this.conn) {
+        return { error: "wrongOrder",
+                 message: "trying to resume in the wrong order.",
+                 lastPausedUrl: lastNestRequestor.url };
+      }
+    }
+
     if (aRequest && aRequest.forceCompletion) {
       // TODO: remove this when Debugger.Frame.prototype.pop is implemented in
       // bug 736733.
       if (typeof this.frame.pop != "function") {
         return { error: "notImplemented",
                  message: "forced completion is not yet implemented." };
       }
 
@@ -723,17 +735,20 @@ ThreadActor.prototype = {
     return packet;
   },
 
   _nest: function TA_nest() {
     if (this._hooks.preNest) {
       var nestData = this._hooks.preNest();
     }
 
-    DebuggerServer.xpcInspector.enterNestedEventLoop();
+    let requestor = Object.create(null);
+    requestor.url = this._hooks.url;
+    requestor.connection = this.conn;
+    DebuggerServer.xpcInspector.enterNestedEventLoop(requestor);
 
     dbg_assert(this.state === "running");
 
     if (this._hooks.postNest) {
       this._hooks.postNest(nestData)
     }
 
     // "continue" resumption value.
--- a/toolkit/devtools/debugger/tests/unit/test_nsjsinspector.js
+++ b/toolkit/devtools/debugger/tests/unit/test_nsjsinspector.js
@@ -1,22 +1,59 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+// Test the basic functionality of the nsIJSInspector component.
+var gCount = 0;
+const MAX = 10;
+var inspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
+var tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+
+// Emulate 10 simultaneously-debugged windows from 3 separate client connections.
+var requestor = (count) => ({
+  url:"http://foo/bar/" + count,
+  connection: "conn" + (count % 3)
+});
+
 function run_test()
 {
   test_nesting();
 }
 
 function test_nesting()
 {
-  let inspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
   do_check_eq(inspector.eventLoopNestLevel, 0);
 
-  let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
-  tm.currentThread.dispatch({ run: function() {
-    do_check_eq(inspector.eventLoopNestLevel, 1);
-    do_check_eq(inspector.exitNestedEventLoop(), 0);
-  }}, 0);
+  tm.currentThread.dispatch({ run: enterEventLoop}, 0);
+
+  do_check_eq(inspector.enterNestedEventLoop(requestor(gCount)), 0);
+  do_check_eq(inspector.eventLoopNestLevel, 0);
+  do_check_eq(inspector.lastNestRequestor, null);
+}
+
+function enterEventLoop() {
+  if (gCount++ < MAX) {
+    tm.currentThread.dispatch({ run: enterEventLoop}, 0);
+
+    let r = Object.create(requestor(gCount));
 
-  do_check_eq(inspector.enterNestedEventLoop(), 0);
-  do_check_eq(inspector.eventLoopNestLevel, 0);
-}
\ No newline at end of file
+    do_check_eq(inspector.eventLoopNestLevel, gCount);
+    do_check_eq(inspector.lastNestRequestor.url, requestor(gCount - 1).url);
+    do_check_eq(inspector.lastNestRequestor.connection, requestor(gCount - 1).connection);
+    do_check_eq(inspector.enterNestedEventLoop(requestor(gCount)), gCount);
+  } else {
+    do_check_eq(gCount, MAX + 1);
+    tm.currentThread.dispatch({ run: exitEventLoop}, 0);
+  }
+}
+
+function exitEventLoop() {
+  if (inspector.lastNestRequestor != null) {
+    do_check_eq(inspector.lastNestRequestor.url, requestor(gCount - 1).url);
+    do_check_eq(inspector.lastNestRequestor.connection, requestor(gCount - 1).connection);
+    if (gCount-- > 1) {
+      tm.currentThread.dispatch({ run: exitEventLoop}, 0);
+    }
+
+    do_check_eq(inspector.exitNestedEventLoop(), gCount);
+    do_check_eq(inspector.eventLoopNestLevel, gCount);
+  }
+}