diff --git a/browser/devtools/debugger/DebuggerUI.jsm b/browser/devtools/debugger/DebuggerUI.jsm index 45ccf911adc..6b172b88af5 100644 --- a/browser/devtools/debugger/DebuggerUI.jsm +++ b/browser/devtools/debugger/DebuggerUI.jsm @@ -22,7 +22,7 @@ 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. @@ -71,7 +71,7 @@ DebuggerUI.prototype = { * @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) { @@ -110,34 +110,55 @@ DebuggerUI.prototype = { 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; }, /** @@ -170,14 +191,17 @@ DebuggerUI.prototype = { 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) }]; @@ -204,7 +228,7 @@ DebuggerUI.prototype = { * 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; @@ -229,7 +253,7 @@ DebuggerPane.prototype = { * 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; @@ -260,7 +284,7 @@ DebuggerPane.prototype = { }, true); this._frame.setAttribute("src", DBG_XUL); - this._globalUI.refreshCommand(); + this.globalUI.refreshCommand(); }, /** @@ -271,10 +295,10 @@ DebuggerPane.prototype = { * 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; @@ -299,7 +323,8 @@ DebuggerPane.prototype = { this._frame = null; this._nbox = null; - this._globalUI.refreshCommand(); + this.globalUI.refreshCommand(); + this.globalUI = null; }, /** @@ -338,7 +363,7 @@ DebuggerPane.prototype = { * The parent instance creating the new debugger. */ function RemoteDebuggerWindow(aDebuggerUI) { - this._globalUI = aDebuggerUI; + this.globalUI = aDebuggerUI; this._win = aDebuggerUI.chromeWindow; this._create(); @@ -350,9 +375,9 @@ 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 + "," + @@ -380,10 +405,11 @@ RemoteDebuggerWindow.prototype = { * 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(); @@ -414,15 +440,16 @@ RemoteDebuggerWindow.prototype = { /** * 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; @@ -494,7 +521,7 @@ ChromeDebuggerProcess.prototype = { * 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" @@ -521,10 +548,11 @@ ChromeDebuggerProcess.prototype = { * 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) { diff --git a/browser/devtools/debugger/test/Makefile.in b/browser/devtools/debugger/test/Makefile.in index 103539e00c7..cd9e532a8a2 100644 --- a/browser/devtools/debugger/test/Makefile.in +++ b/browser/devtools/debugger/test/Makefile.in @@ -15,6 +15,7 @@ _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 \ diff --git a/browser/devtools/debugger/test/browser_dbg_debugger-tab-switch-window.js b/browser/devtools/debugger/test/browser_dbg_debugger-tab-switch-window.js new file mode 100644 index 00000000000..d6b17c61c63 --- /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); +} diff --git a/browser/devtools/debugger/test/browser_dbg_debugger-tab-switch.js b/browser/devtools/debugger/test/browser_dbg_debugger-tab-switch.js index 5e97efc70d1..d7e3e00f1c4 100644 --- a/browser/devtools/debugger/test/browser_dbg_debugger-tab-switch.js +++ b/browser/devtools/debugger/test/browser_dbg_debugger-tab-switch.js @@ -8,6 +8,12 @@ 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); @@ -50,7 +56,7 @@ function testTab1(callback) { executeSoon(function() { callback(); }); - }, true); + }); }); } @@ -208,8 +214,8 @@ function lastTest(callback) { }); } -function cleanup(callback) { - +function cleanup(callback) +{ gPane1 = null; gPane2 = null; gNbox = null; diff --git a/browser/devtools/debugger/test/head.js b/browser/devtools/debugger/test/head.js index 91529470e35..38944956b7a 100644 --- a/browser/devtools/debugger/test/head.js +++ b/browser/devtools/debugger/test/head.js @@ -33,11 +33,26 @@ if (!DebuggerServer.initialized) { 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); - let tab = gBrowser.selectedTab; + return chromeWindow; +} + +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) { @@ -51,11 +66,17 @@ function addTab(aURL, aOnload) 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; @@ -67,19 +88,19 @@ function closeDebuggerAndFinish(aRemoteFlag, 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(); } } @@ -132,10 +153,13 @@ function debug_tab_pane(aURL, aOnDebugging) }); } -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() {