author | Ryan VanderMeulen <ryanvm@gmail.com> |
Tue, 09 Apr 2013 15:29:13 -0400 | |
changeset 128204 | 9db46ddfb517f302ba8039560247f4abd26581d5 |
parent 128192 | 974726290de1761f5df6d3fda7ff79dacd0e4979 (current diff) |
parent 128203 | 9d5f05a6d497f968fd1580f6af8f84486323b0c3 (diff) |
child 128205 | 52a3f612300983c750c889b4c883133ede1c8b28 |
push id | 26193 |
push user | ryanvm@gmail.com |
push date | Tue, 09 Apr 2013 19:29:36 +0000 |
treeherder | mozilla-inbound@9db46ddfb517 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 23.0a1 |
first release with | nightly linux32
9db46ddfb517
/
23.0a1
/
20130410031044
/
files
nightly linux64
9db46ddfb517
/
23.0a1
/
20130410031044
/
files
nightly mac
9db46ddfb517
/
23.0a1
/
20130410031044
/
files
nightly win32
9db46ddfb517
/
23.0a1
/
20130410031044
/
files
nightly win64
9db46ddfb517
/
23.0a1
/
20130410031044
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
23.0a1
/
20130410031044
/
pushlog to previous
nightly linux64
23.0a1
/
20130410031044
/
pushlog to previous
nightly mac
23.0a1
/
20130410031044
/
pushlog to previous
nightly win32
23.0a1
/
20130410031044
/
pushlog to previous
nightly win64
23.0a1
/
20130410031044
/
pushlog to previous
|
browser/base/content/browser.js | file | annotate | diff | comparison | revisions | |
browser/devtools/debugger/test/browser_dbg_debugger-tab-switch-window.js | file | annotate | diff | comparison | revisions | |
browser/devtools/debugger/test/browser_dbg_debugger-tab-switch.js | file | annotate | diff | comparison | revisions | |
browser/devtools/webconsole/test/browser_webconsole_property_panel.js | file | annotate | diff | comparison | revisions | |
js/src/vm/Debugger.cpp | file | annotate | diff | comparison | revisions |
--- a/browser/base/content/browser-appmenu.inc +++ b/browser/base/content/browser-appmenu.inc @@ -144,16 +144,18 @@ <menupopup id="appmenu_webDeveloper_popup"> <menuitem id="appmenu_devToolbox" observes="devtoolsMenuBroadcaster_DevToolbox"/> <menuseparator id="appmenu_devtools_separator"/> <menuitem id="appmenu_devToolbar" observes="devtoolsMenuBroadcaster_DevToolbar"/> <menuitem id="appmenu_chromeDebugger" observes="devtoolsMenuBroadcaster_ChromeDebugger"/> + <menuitem id="appmenu_browserConsole" + observes="devtoolsMenuBroadcaster_BrowserConsole"/> <menuitem id="appmenu_responsiveUI" observes="devtoolsMenuBroadcaster_ResponsiveUI"/> <menuitem id="appmenu_scratchpad" observes="devtoolsMenuBroadcaster_Scratchpad"/> <menuitem id="appmenu_pageSource" observes="devtoolsMenuBroadcaster_PageSource"/> <menuitem id="appmenu_errorConsole" observes="devtoolsMenuBroadcaster_ErrorConsole"/>
--- a/browser/base/content/browser-menubar.inc +++ b/browser/base/content/browser-menubar.inc @@ -571,16 +571,18 @@ observes="devtoolsMenuBroadcaster_DevToolbox" accesskey="&devToolboxMenuItem.accesskey;"/> <menuseparator id="menu_devtools_separator"/> <menuitem id="menu_devToolbar" observes="devtoolsMenuBroadcaster_DevToolbar" accesskey="&devToolbarMenu.accesskey;"/> <menuitem id="menu_chromeDebugger" observes="devtoolsMenuBroadcaster_ChromeDebugger"/> + <menuitem id="menu_browserConsole" + observes="devtoolsMenuBroadcaster_BrowserConsole"/> <menuitem id="menu_responsiveUI" observes="devtoolsMenuBroadcaster_ResponsiveUI" accesskey="&responsiveDesignTool.accesskey;"/> <menuitem id="menu_scratchpad" observes="devtoolsMenuBroadcaster_Scratchpad" accesskey="&scratchpad.accesskey;"/> <menuitem id="menu_pageSource" observes="devtoolsMenuBroadcaster_PageSource"
--- a/browser/base/content/browser-sets.inc +++ b/browser/base/content/browser-sets.inc @@ -91,16 +91,17 @@ <command id="Browser:RestoreLastSession" oncommand="restoreLastSession();" disabled="true"/> <command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/> <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/> <command id="Tools:DevToolbox" oncommand="gDevToolsBrowser.toggleToolboxCommand(gBrowser);"/> <command id="Tools:DevToolbar" oncommand="DeveloperToolbar.toggle();" disabled="true" hidden="true"/> <command id="Tools:DevToolbarFocus" oncommand="DeveloperToolbar.focusToggle();" disabled="true"/> <command id="Tools:ChromeDebugger" oncommand="DebuggerUI.toggleChromeDebugger();" disabled="true" hidden="true"/> + <command id="Tools:BrowserConsole" oncommand="HUDConsoleUI.toggleBrowserConsole();" disabled="true" hidden="true"/> <command id="Tools:Scratchpad" oncommand="Scratchpad.openScratchpad();" disabled="true" hidden="true"/> <command id="Tools:ResponsiveUI" oncommand="ResponsiveUI.toggle();" disabled="true" hidden="true"/> <command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/> <command id="Tools:ErrorConsole" oncommand="toJavaScriptConsole()" disabled="true" hidden="true"/> <command id="Tools:DevToolsConnect" oncommand="gDevToolsBrowser.openConnectScreen(gBrowser)" disabled="true" hidden="true"/> <command id="Tools:Sanitize" oncommand="Cc['@mozilla.org/browser/browserglue;1'].getService(Ci.nsIBrowserGlue).sanitize(window);"/> <command id="Tools:PrivateBrowsing" @@ -179,16 +180,19 @@ <broadcaster id="devtoolsMenuBroadcaster_DevToolbar" label="&devToolbarMenu.label;" type="checkbox" autocheck="false" command="Tools:DevToolbar" key="key_devToolbar"/> <broadcaster id="devtoolsMenuBroadcaster_ChromeDebugger" label="&chromeDebuggerMenu.label;" command="Tools:ChromeDebugger"/> + <broadcaster id="devtoolsMenuBroadcaster_BrowserConsole" + label="&browserConsoleCmd.label;" + command="Tools:BrowserConsole"/> <broadcaster id="devtoolsMenuBroadcaster_Scratchpad" label="&scratchpad.label;" command="Tools:Scratchpad" key="key_scratchpad"/> <broadcaster id="devtoolsMenuBroadcaster_ResponsiveUI" label="&responsiveDesignTool.label;" type="checkbox" autocheck="false" command="Tools:ResponsiveUI"
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -1566,29 +1566,37 @@ var gBrowserInit = { // Show the toolbar if it was previously visible if (gPrefService.getBoolPref("devtools.toolbar.visible")) { DeveloperToolbar.show(false); } } // Enable Chrome Debugger? - let enabled = gPrefService.getBoolPref("devtools.chrome.enabled") && - gPrefService.getBoolPref("devtools.debugger.chrome-enabled") && - gPrefService.getBoolPref("devtools.debugger.remote-enabled"); - if (enabled) { + let chromeEnabled = gPrefService.getBoolPref("devtools.chrome.enabled"); + let remoteEnabled = chromeEnabled && + gPrefService.getBoolPref("devtools.debugger.chrome-enabled") && + gPrefService.getBoolPref("devtools.debugger.remote-enabled"); + if (remoteEnabled) { let cmd = document.getElementById("Tools:ChromeDebugger"); cmd.removeAttribute("disabled"); cmd.removeAttribute("hidden"); } + // Enable the Browser Console? + if (chromeEnabled) { + let cmd = document.getElementById("Tools:BrowserConsole"); + cmd.removeAttribute("disabled"); + cmd.removeAttribute("hidden"); + } + // Enable Error Console? // Temporarily enabled. See bug 798925. let consoleEnabled = true || gPrefService.getBoolPref("devtools.errorconsole.enabled") || - gPrefService.getBoolPref("devtools.chrome.enabled"); + chromeEnabled; if (consoleEnabled) { let cmd = document.getElementById("Tools:ErrorConsole"); cmd.removeAttribute("disabled"); cmd.removeAttribute("hidden"); } // Enable Scratchpad in the UI, if the preference allows this. let scratchpadEnabled = gPrefService.getBoolPref(Scratchpad.prefEnabledName);
--- 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 @@ -10,27 +10,16 @@ const Ci = Components.interfaces; const Cu = Components.utils; const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties"; const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "self-hosted"]; const NEW_SOURCE_DISPLAY_DELAY = 200; // ms const FETCH_SOURCE_RESPONSE_DELAY = 50; // ms const FRAME_STEP_CLEAR_DELAY = 100; // ms const CALL_STACK_PAGE_SIZE = 25; // frames -const VARIABLES_VIEW_NON_SORTABLE = [ - "Array", - "Int8Array", - "Uint8Array", - "Int16Array", - "Uint16Array", - "Int32Array", - "Uint32Array", - "Float32Array", - "Float64Array" -]; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/devtools/dbg-server.jsm"); Cu.import("resource://gre/modules/devtools/dbg-client.jsm"); Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); Cu.import("resource:///modules/source-editor.jsm"); Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); @@ -262,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. @@ -298,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(); } }); }, /** @@ -523,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) { @@ -888,17 +886,17 @@ StackFrames.prototype = { if (aVar._fetched) { return; } aVar._fetched = true; let grip = aVar._sourceGrip; this.activeThread.pauseGrip(grip).getPrototypeAndProperties(function(aResponse) { let { ownProperties, prototype } = aResponse; - let sortable = VARIABLES_VIEW_NON_SORTABLE.indexOf(grip.class) == -1; + let sortable = VariablesView.NON_SORTABLE_CLASSES.indexOf(grip.class) == -1; // Add all the variable properties. if (ownProperties) { aVar.addProperties(ownProperties, { // Not all variables need to force sorted properties. sorted: sortable, // Expansion handlers must be set after the properties are added. callback: this._addVarExpander
--- 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/devtools/framework/Sidebar.jsm +++ b/browser/devtools/framework/Sidebar.jsm @@ -56,24 +56,24 @@ ToolSidebar.prototype = { iframe.setAttribute("src", url); iframe.tooltip = "aHTMLTooltip"; let tab = this._tabbox.tabs.appendItem(); tab.setAttribute("label", ""); // Avoid showing "undefined" while the tab is loading let onIFrameLoaded = function() { tab.setAttribute("label", iframe.contentDocument.title); - iframe.removeEventListener("DOMContentLoaded", onIFrameLoaded, true); + iframe.removeEventListener("load", onIFrameLoaded, true); if ("setPanel" in iframe.contentWindow) { iframe.contentWindow.setPanel(this._toolPanel, iframe); } this.emit(id + "-ready"); }.bind(this); - iframe.addEventListener("DOMContentLoaded", onIFrameLoaded, true); + iframe.addEventListener("load", onIFrameLoaded, true); let tabpanel = this._panelDoc.createElementNS(XULNS, "tabpanel"); tabpanel.setAttribute("id", "sidebar-panel-" + id); tabpanel.appendChild(iframe); this._tabbox.tabpanels.appendChild(tabpanel); this._tooltip = this._panelDoc.createElementNS(XULNS, "tooltip"); this._tooltip.id = "aHTMLTooltip"; @@ -86,17 +86,17 @@ ToolSidebar.prototype = { this._tabs.set(id, tab); if (selected) { // For some reason I don't understand, if we call this.select in this // event loop (after inserting the tab), the tab will never get the // the "selected" attribute set to true. this._panelDoc.defaultView.setTimeout(function() { this.select(id); - }.bind(this), 0); + }.bind(this), 10); } this.emit("new-tab-registered", id); }, /** * Select a specific tab. */
--- a/browser/devtools/framework/test/browser_toolbox_sidebar.js +++ b/browser/devtools/framework/test/browser_toolbox_sidebar.js @@ -17,17 +17,17 @@ function test() { "</hbox>" + "</window>"; const tab1URL = "data:text/html;charset=utf8,<title>1</title><p>1</p>"; const tab2URL = "data:text/html;charset=utf8,<title>2</title><p>2</p>"; const tab3URL = "data:text/html;charset=utf8,<title>3</title><p>3</p>"; let panelDoc; - + let tab1Selected = false; let registeredTabs = {}; let readyTabs = {}; let toolDefinition = { id: "fakeTool4242", killswitch: "devtools.fakeTool4242.enabled", url: toolURL, label: "FAKE TOOL!!!", @@ -58,45 +58,52 @@ function test() { let tabbox = panel.panelDoc.getElementById("sidebar"); panel.sidebar = new ToolSidebar(tabbox, panel, true); panel.sidebar.on("new-tab-registered", function(event, id) { registeredTabs[id] = true; }); panel.sidebar.once("tab1-ready", function(event) { + info(event); readyTabs.tab1 = true; - if (readyTabs.tab1 && readyTabs.tab2 && readyTabs.tab3) { - allTabsReady(panel); - } + allTabsReady(panel); }); panel.sidebar.once("tab2-ready", function(event) { + info(event); readyTabs.tab2 = true; - if (readyTabs.tab1 && readyTabs.tab2 && readyTabs.tab3) { - allTabsReady(panel); - } + allTabsReady(panel); }); panel.sidebar.once("tab3-ready", function(event) { + info(event); readyTabs.tab3 = true; - if (readyTabs.tab1 && readyTabs.tab2 && readyTabs.tab3) { - allTabsReady(panel); - } + allTabsReady(panel); + }); + + panel.sidebar.once("tab1-selected", function(event) { + info(event); + tab1Selected = true; + allTabsReady(panel); }); panel.sidebar.addTab("tab1", tab1URL, true); panel.sidebar.addTab("tab2", tab2URL); panel.sidebar.addTab("tab3", tab3URL); panel.sidebar.show(); }).then(null, console.error); }); function allTabsReady(panel) { + if (!tab1Selected || !readyTabs.tab1 || !readyTabs.tab2 || !readyTabs.tab3) { + return; + } + ok(registeredTabs.tab1, "tab1 registered"); ok(registeredTabs.tab2, "tab2 registered"); ok(registeredTabs.tab3, "tab3 registered"); ok(readyTabs.tab1, "tab1 ready"); ok(readyTabs.tab2, "tab2 ready"); ok(readyTabs.tab3, "tab3 ready"); let tabs = panel.sidebar._tabbox.querySelectorAll("tab");
--- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -1,14 +1,15 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. browser.jar: content/browser/devtools/widgets.css (shared/widgets/widgets.css) + content/browser/devtools/widgets/VariablesView.xul (shared/widgets/VariablesView.xul) content/browser/devtools/markup-view.xhtml (markupview/markup-view.xhtml) content/browser/devtools/markup-view.css (markupview/markup-view.css) content/browser/devtools/netmonitor.xul (netmonitor/netmonitor.xul) content/browser/devtools/netmonitor.css (netmonitor/netmonitor.css) content/browser/devtools/netmonitor-controller.js (netmonitor/netmonitor-controller.js) content/browser/devtools/netmonitor-view.js (netmonitor/netmonitor-view.js) content/browser/NetworkPanel.xhtml (webconsole/NetworkPanel.xhtml) content/browser/devtools/webconsole.js (webconsole/webconsole.js)
--- a/browser/devtools/netmonitor/netmonitor-view.js +++ b/browser/devtools/netmonitor/netmonitor-view.js @@ -487,28 +487,28 @@ create({ constructor: RequestsMenuView, case "status": { let node = $(".requests-menu-status", aItem.target); node.setAttribute("code", aValue); break; } case "contentSize": { let size = (aValue / 1024).toFixed(CONTENT_SIZE_DECIMALS); let node = $(".requests-menu-size", aItem.target); - node.setAttribute("value", L10N.getFormatStr("networkMenu.size", size)); + node.setAttribute("value", L10N.getFormatStr("networkMenu.sizeKB", size)); break; } case "mimeType": { let type = aValue.split(";")[0].split("/")[1] || "?"; let node = $(".requests-menu-type", aItem.target); node.setAttribute("value", CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type); break; } case "totalTime": { let node = $(".requests-menu-timings-total", aItem.target); - node.setAttribute("value", L10N.getFormatStr("networkMenu.total", aValue)); + node.setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", aValue)); break; } } }, /** * Creates a waterfall representing timing information in a network request item view. * @@ -853,17 +853,17 @@ create({ constructor: NetworkDetailsView * * @param string aName * The type of headers to populate (request or response). * @param object aResponse * The message received from the server. */ _addHeaders: function NVND__addHeaders(aName, aResponse) { let kb = (aResponse.headersSize / 1024).toFixed(HEADERS_SIZE_DECIMALS); - let size = L10N.getFormatStr("networkMenu.size", kb); + let size = L10N.getFormatStr("networkMenu.sizeKB", kb); let headersScope = this._headers.addScope(aName + " (" + size + ")"); headersScope.expanded = true; for (let header of aResponse.headers) { let headerVar = headersScope.addVar(header.name, { null: true }, true); headerVar.setGrip(header.value); } }, @@ -1078,42 +1078,42 @@ create({ constructor: NetworkDetailsView let tabboxWidth = $("#details-pane").getAttribute("width"); let availableWidth = tabboxWidth / 2; // Other nodes also take some space. let scale = Math.max(availableWidth / aResponse.totalTime, 0); $("#timings-summary-blocked .requests-menu-timings-box") .setAttribute("width", blocked * scale); $("#timings-summary-blocked .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.total", blocked)); + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", blocked)); $("#timings-summary-dns .requests-menu-timings-box") .setAttribute("width", dns * scale); $("#timings-summary-dns .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.total", dns)); + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", dns)); $("#timings-summary-connect .requests-menu-timings-box") .setAttribute("width", connect * scale); $("#timings-summary-connect .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.total", connect)); + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", connect)); $("#timings-summary-send .requests-menu-timings-box") .setAttribute("width", send * scale); $("#timings-summary-send .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.total", send)); + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", send)); $("#timings-summary-wait .requests-menu-timings-box") .setAttribute("width", wait * scale); $("#timings-summary-wait .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.total", wait)); + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", wait)); $("#timings-summary-receive .requests-menu-timings-box") .setAttribute("width", receive * scale); $("#timings-summary-receive .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.total", receive)); + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", receive)); $("#timings-summary-dns .requests-menu-timings-box") .style.transform = "translateX(" + (scale * blocked) + "px)"; $("#timings-summary-connect .requests-menu-timings-box") .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)"; $("#timings-summary-send .requests-menu-timings-box") .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)"; $("#timings-summary-wait .requests-menu-timings-box")
--- a/browser/devtools/netmonitor/test/browser_net_content-type.js +++ b/browser/devtools/netmonitor/test/browser_net_content-type.js @@ -4,62 +4,62 @@ /** * Tests if different response content types are handled correctly. */ function test() { initNetMonitor(CONTENT_TYPE_URL).then(([aTab, aDebuggee, aMonitor]) => { info("Starting test... "); - let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin; + let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin; let { RequestsMenu } = NetMonitorView; RequestsMenu.lazyUpdate = false; waitForNetworkEvents(aMonitor, 6).then(() => { verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0), "GET", CONTENT_TYPE_SJS + "?fmt=xml", { status: 200, type: "xml", - size: "0.04kb", + size: L10N.getFormatStr("networkMenu.sizeKB", 0.04), time: true }); verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1), "GET", CONTENT_TYPE_SJS + "?fmt=css", { status: 200, type: "css", - size: "0.03kb", + size: L10N.getFormatStr("networkMenu.sizeKB", 0.03), time: true }); verifyRequestItemTarget(RequestsMenu.getItemAtIndex(2), "GET", CONTENT_TYPE_SJS + "?fmt=js", { status: 200, type: "js", - size: "0.03kb", + size: L10N.getFormatStr("networkMenu.sizeKB", 0.03), time: true }); verifyRequestItemTarget(RequestsMenu.getItemAtIndex(3), "GET", CONTENT_TYPE_SJS + "?fmt=json", { status: 200, type: "json", - size: "0.03kb", + size: L10N.getFormatStr("networkMenu.sizeKB", 0.03), time: true }); verifyRequestItemTarget(RequestsMenu.getItemAtIndex(4), "GET", CONTENT_TYPE_SJS + "?fmt=bogus", { status: 404, type: "html", - size: "0.02kb", + size: L10N.getFormatStr("networkMenu.sizeKB", 0.02), time: true }); verifyRequestItemTarget(RequestsMenu.getItemAtIndex(5), "GET", TEST_IMAGE, { status: 200, type: "png", - size: "0.76kb", + size: L10N.getFormatStr("networkMenu.sizeKB", 0.76), time: true }); EventUtils.sendMouseEvent({ type: "mousedown" }, document.getElementById("details-pane-toggle")); EventUtils.sendMouseEvent({ type: "mousedown" }, document.querySelectorAll("#details-pane tab")[3]);
--- a/browser/devtools/netmonitor/test/browser_net_post-data.js +++ b/browser/devtools/netmonitor/test/browser_net_post-data.js @@ -15,24 +15,24 @@ function test() { RequestsMenu.lazyUpdate = false; NetworkDetails._params.lazyEmpty = false; waitForNetworkEvents(aMonitor, 0, 2).then(() => { verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0), "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=urlencoded", { status: 200, type: "plain", - size: "0.01kb", + size: L10N.getFormatStr("networkMenu.sizeKB", 0.01), time: true }); verifyRequestItemTarget(RequestsMenu.getItemAtIndex(1), "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=multipart", { status: 200, type: "plain", - size: "0.01kb", + size: L10N.getFormatStr("networkMenu.sizeKB", 0.01), time: true }); EventUtils.sendMouseEvent({ type: "mousedown" }, document.getElementById("details-pane-toggle")); EventUtils.sendMouseEvent({ type: "mousedown" }, document.querySelectorAll("#details-pane tab")[2]);
--- a/browser/devtools/netmonitor/test/browser_net_prefs-and-l10n.js +++ b/browser/devtools/netmonitor/test/browser_net_prefs-and-l10n.js @@ -22,18 +22,18 @@ function test() { let bundleName = "chrome://browser/locale/devtools/netmonitor.properties"; let stringBundle = Services.strings.createBundle(bundleName); is(L10N.getStr("netmonitor.label"), stringBundle.GetStringFromName("netmonitor.label"), "The getStr() method didn't return the expected string."); - is(L10N.getFormatStr("networkMenu.total", "foo"), - stringBundle.formatStringFromName("networkMenu.total", ["foo"], 1), + is(L10N.getFormatStr("networkMenu.totalMS", "foo"), + stringBundle.formatStringFromName("networkMenu.totalMS", ["foo"], 1), "The getFormatStr() method didn't return the expected string."); } function testPrefs() { let { Prefs } = aMonitor.panelWin; is(Prefs.root, "devtools.netmonitor", "The preferences object should have a correct root path.");
--- a/browser/devtools/netmonitor/test/browser_net_simple-request-data.js +++ b/browser/devtools/netmonitor/test/browser_net_simple-request-data.js @@ -4,17 +4,17 @@ /** * Tests if requests render correct information in the menu UI. */ function test() { initNetMonitor(SIMPLE_SJS).then(([aTab, aDebuggee, aMonitor]) => { info("Starting test... "); - let { NetMonitorView } = aMonitor.panelWin; + let { L10N, NetMonitorView } = aMonitor.panelWin; let { RequestsMenu } = NetMonitorView; RequestsMenu.lazyUpdate = false; waitForNetworkEvents(aMonitor, 1) .then(() => teardown(aMonitor)) .then(finish); @@ -157,17 +157,17 @@ function test() { is(requestItem.attachment.contentSize, "12", "The contentSize attachment has an incorrect value."); is(requestItem.attachment.mimeType, "text/plain; charset=utf-8", "The mimeType attachment has an incorrect value."); verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, { type: "plain", - size: "0.01kb" + size: L10N.getFormatStr("networkMenu.sizeKB", 0.01), }); }); aMonitor.panelWin.once("NetMonitor:NetworkEventUpdated:ResponseContent", () => { let requestItem = RequestsMenu.getItemAtIndex(0); ok(requestItem.attachment.responseContent, "There should be a responseContent attachment available."); @@ -175,17 +175,17 @@ function test() { "The responseContent attachment has an incorrect |content.mimeType| property."); is(requestItem.attachment.responseContent.content.text, "Hello world!", "The responseContent attachment has an incorrect |content.text| property."); is(requestItem.attachment.responseContent.content.size, 12, "The responseContent attachment has an incorrect |content.size| property."); verifyRequestItemTarget(requestItem, "GET", SIMPLE_SJS, { type: "plain", - size: "0.01kb" + size: L10N.getFormatStr("networkMenu.sizeKB", 0.01), }); }); aMonitor.panelWin.once("NetMonitor:NetworkEventUpdating:EventTimings", () => { let requestItem = RequestsMenu.getItemAtIndex(0); is(typeof requestItem.attachment.totalTime, "number", "The attached totalTime is incorrect.");
--- a/browser/devtools/netmonitor/test/browser_net_simple-request-details.js +++ b/browser/devtools/netmonitor/test/browser_net_simple-request-details.js @@ -66,17 +66,17 @@ function test() { is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0, "The empty notice should not be displayed in this tabpanel."); let responseScope = tabpanel.querySelectorAll(".variables-view-scope")[0]; let requestScope = tabpanel.querySelectorAll(".variables-view-scope")[1]; is(responseScope.querySelector(".name").getAttribute("value"), L10N.getStr("responseHeaders") + " (" + - L10N.getFormatStr("networkMenu.size", "0.169") + ")", + L10N.getFormatStr("networkMenu.sizeKB", "0.169") + ")", "The response headers scope doesn't have the correct title."); ok(requestScope.querySelector(".name").getAttribute("value").contains( L10N.getStr("requestHeaders") + " (0."), // Can't test for full request headers title because the size may // vary across platforms ("User-Agent" header differs). "The request headers scope doesn't have the correct title."); @@ -191,35 +191,35 @@ function test() { let tab = document.querySelectorAll("#details-pane tab")[4]; let tabpanel = document.querySelectorAll("#details-pane tabpanel")[4]; is(tab.getAttribute("selected"), "true", "The timings tab in the network details pane should be selected."); ok(tabpanel.querySelector("#timings-summary-blocked .requests-menu-timings-total") - .getAttribute("value").match(/[0-9]+ms$/), + .getAttribute("value").match(/[0-9]+/), "The blocked timing info does not appear to be correct."); ok(tabpanel.querySelector("#timings-summary-dns .requests-menu-timings-total") - .getAttribute("value").match(/[0-9]+ms$/), + .getAttribute("value").match(/[0-9]+/), "The dns timing info does not appear to be correct."); ok(tabpanel.querySelector("#timings-summary-connect .requests-menu-timings-total") - .getAttribute("value").match(/[0-9]+ms$/), + .getAttribute("value").match(/[0-9]+/), "The connect timing info does not appear to be correct."); ok(tabpanel.querySelector("#timings-summary-send .requests-menu-timings-total") - .getAttribute("value").match(/[0-9]+ms$/), + .getAttribute("value").match(/[0-9]+/), "The send timing info does not appear to be correct."); ok(tabpanel.querySelector("#timings-summary-wait .requests-menu-timings-total") - .getAttribute("value").match(/[0-9]+ms$/), + .getAttribute("value").match(/[0-9]+/), "The wait timing info does not appear to be correct."); ok(tabpanel.querySelector("#timings-summary-receive .requests-menu-timings-total") - .getAttribute("value").match(/[0-9]+ms$/), + .getAttribute("value").match(/[0-9]+/), "The receive timing info does not appear to be correct."); } aDebuggee.location.reload(); }); }
--- a/browser/devtools/netmonitor/test/browser_net_status-codes.js +++ b/browser/devtools/netmonitor/test/browser_net_status-codes.js @@ -17,45 +17,45 @@ function test() { waitForNetworkEvents(aMonitor, 5).then(() => { let requestItems = []; verifyRequestItemTarget(requestItems[0] = RequestsMenu.getItemAtIndex(0), "GET", STATUS_CODES_SJS + "?sts=100", { status: 101, type: "plain", - size: "0.00kb", + size: L10N.getFormatStr("networkMenu.sizeKB", "0.00"), time: true }); verifyRequestItemTarget(requestItems[1] = RequestsMenu.getItemAtIndex(1), "GET", STATUS_CODES_SJS + "?sts=200", { status: 202, type: "plain", - size: "0.02kb", + size: L10N.getFormatStr("networkMenu.sizeKB", 0.02), time: true }); verifyRequestItemTarget(requestItems[2] = RequestsMenu.getItemAtIndex(2), "GET", STATUS_CODES_SJS + "?sts=300", { status: 303, type: "plain", - size: "0.00kb", + size: L10N.getFormatStr("networkMenu.sizeKB", "0.00"), time: true }); verifyRequestItemTarget(requestItems[3] = RequestsMenu.getItemAtIndex(3), "GET", STATUS_CODES_SJS + "?sts=400", { status: 404, type: "plain", - size: "0.02kb", + size: L10N.getFormatStr("networkMenu.sizeKB", 0.02), time: true }); verifyRequestItemTarget(requestItems[4] = RequestsMenu.getItemAtIndex(4), "GET", STATUS_CODES_SJS + "?sts=500", { status: 501, type: "plain", - size: "0.02kb", + size: L10N.getFormatStr("networkMenu.sizeKB", 0.02), time: true }); // Test summaries... EventUtils.sendMouseEvent({ type: "mousedown" }, document.querySelectorAll("#details-pane tab")[0]); EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[0].target);
--- a/browser/devtools/shared/widgets/VariablesView.jsm +++ b/browser/devtools/shared/widgets/VariablesView.jsm @@ -913,16 +913,28 @@ VariablesView.prototype = { _boxObject: null, _searchboxNode: null, _searchboxContainer: null, _searchboxPlaceholder: "", _emptyTextNode: null, _emptyTextValue: "" }; +VariablesView.NON_SORTABLE_CLASSES = [ + "Array", + "Int8Array", + "Uint8Array", + "Int16Array", + "Uint16Array", + "Int32Array", + "Uint32Array", + "Float32Array", + "Float64Array" +]; + /** * Generates the string evaluated when performing simple value changes. * * @param Variable | Property aItem * The current variable or property. * @param string aCurrentString * The trimmed user inputted string. * @return string @@ -978,17 +990,18 @@ VariablesView.getterOrSetterEvalMacro = case "undefined": let mirrorType = type == "get" ? "set" : "get"; let mirrorLookup = type == "get" ? "__lookupSetter__" : "__lookupGetter__"; // If the parent object will end up without any getter or setter, // morph it into a plain value. if ((type == "set" && propertyObject.getter.type == "undefined") || (type == "get" && propertyObject.setter.type == "undefined")) { - return VariablesView.overrideValueEvalMacro(propertyObject, "undefined"); + // Make sure the right getter/setter to value override macro is applied to the target object. + return propertyObject.evaluationMacro(propertyObject, "undefined"); } // Construct and return the getter/setter removal evaluation string. // e.g: Object.defineProperty(foo, "bar", { // get: foo.__lookupGetter__("bar"), // set: undefined, // enumerable: true, // configurable: true @@ -1036,17 +1049,20 @@ VariablesView.getterOrSetterEvalMacro = /** * Function invoked when a getter or setter is deleted. * * @param Property aItem * The current getter or setter property. */ VariablesView.getterOrSetterDeleteCallback = function(aItem) { aItem._disable(); - aItem.ownerView.eval(VariablesView.getterOrSetterEvalMacro(aItem, "")); + + // Make sure the right getter/setter to value override macro is applied to the target object. + aItem.ownerView.eval(aItem.evaluationMacro(aItem, "")); + return true; // Don't hide the element. }; /** * A Scope is an object holding Variable instances. * Iterable via "for (let [name, variable] in instance) { }". * * @param VariablesView aView @@ -1412,16 +1428,23 @@ Scope.prototype = { if (!item._isExpanded) { return false; } } return true; }, /** + * Focus this scope. + */ + focus: function S_focus() { + this._variablesView._focusItem(this); + }, + + /** * Adds an event listener for a certain event on this scope's title. * @param string aName * @param function aCallback * @param boolean aCapture */ addEventListener: function S_addEventListener(aName, aCallback, aCapture) { this._title.addEventListener(aName, aCallback, aCapture); }, @@ -1444,16 +1467,28 @@ Scope.prototype = { /** * Gets the name associated with this item. * @return string */ get name() this._nameString, /** + * Gets the displayed value for this item. + * @return string + */ + get displayValue() this._valueString, + + /** + * Gets the class names used for the displayed value. + * @return string + */ + get displayValueClassName() this._valueClassName, + + /** * Gets the element associated with this item. * @return nsIDOMNode */ get target() this._target, /** * Initializes this scope's id, view and binds event listeners. * @@ -1520,17 +1555,17 @@ Scope.prototype = { /** * The click listener for this scope's title. */ _onClick: function S__onClick(e) { if (e.target == this._inputNode) { return; } this.toggle(); - this._variablesView._focusItem(this); + this.focus(); }, /** * Lazily appends a node to this scope's enumerable or non-enumerable * container. Once a certain number of nodes have been batched, they * will be appended. * * @param boolean aImmediateFlag @@ -1902,17 +1937,17 @@ ViewHelpers.create({ constructor: Variab * it will be inferred from the value. * e.g. - { someProp0: { value: 42 }, * someProp1: { value: true }, * someProp2: { value: "nasu" }, * someProp3: { value: { type: "undefined" } }, * someProp4: { value: { type: "null" } }, * someProp5: { value: { type: "object", class: "Object" } }, * someProp6: { get: { type: "object", class: "Function" }, - * set: { type: "undefined" } } + * set: { type: "undefined" } } } * @param object aOptions [optional] * Additional options for adding the properties. Supported options: * - sorted: true to sort all the properties before adding them * - callback: function invoked after each property is added */ addProperties: function V_addProperties(aProperties, aOptions = {}) { let propertyNames = Object.keys(aProperties); @@ -2185,16 +2220,17 @@ ViewHelpers.create({ constructor: Variab if (this.ownerView.eval) { this.delete = VariablesView.getterOrSetterDeleteCallback; this.evaluationMacro = VariablesView.overrideValueEvalMacro; } // Deleting getters and setters individually is not allowed if no // evaluation method is provided. else { this.delete = null; + this.evaluationMacro = null; } let getter = this.addProperty("get", { value: descriptor.get }); let setter = this.addProperty("set", { value: descriptor.set }); getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro; setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro; getter.hideArrow(); @@ -2523,40 +2559,40 @@ ViewHelpers.create({ constructor: Variab */ _onNameInputKeyPress: function V__onNameInputKeyPress(e) { e.stopPropagation(); switch(e.keyCode) { case e.DOM_VK_RETURN: case e.DOM_VK_ENTER: this._saveNameInput(e); - this._variablesView._focusItem(this); + this.focus(); return; case e.DOM_VK_ESCAPE: this._deactivateNameInput(e); - this._variablesView._focusItem(this); + this.focus(); return; } }, /** * The key press listener for this variable's editable value textbox. */ _onValueInputKeyPress: function V__onValueInputKeyPress(e) { e.stopPropagation(); switch(e.keyCode) { case e.DOM_VK_RETURN: case e.DOM_VK_ENTER: this._saveValueInput(e); - this._variablesView._focusItem(this); + this.focus(); return; case e.DOM_VK_ESCAPE: this._deactivateValueInput(e); - this._variablesView._focusItem(this); + this.focus(); return; } }, /** * The click listener for the edit button. */ _onEdit: function V__onEdit(e) {
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/widgets/VariablesView.xul @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?> +<!DOCTYPE window [ + <!ENTITY % viewDTD SYSTEM "chrome://browser/locale/devtools/VariablesView.dtd"> + %viewDTD; +]> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&PropertiesViewWindowTitle;"> + <vbox id="variables" flex="1"/> +</window>
--- a/browser/devtools/styleinspector/test/browser_ruleview_focus.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_focus.js @@ -14,19 +14,19 @@ function openRuleView() var target = TargetFactory.forTab(gBrowser.selectedTab); gDevTools.showToolbox(target, "inspector").then(function(toolbox) { inspector = toolbox.getCurrentPanel(); inspector.sidebar.select("ruleview"); // Highlight a node. let node = content.document.getElementsByTagName("h1")[0]; inspector.selection.once("new-node", testFocus); - executeSoon(function() { - inspector.selection.setNode(doc.body); - }); + + inspector.sidebar.once("ruleview-ready", + () => inspector.selection.setNode(doc.body)); }); } function testFocus() { let win = inspector.sidebar.getWindowForTab("ruleview"); let brace = win.document.querySelectorAll(".ruleview-ruleclose")[0];
--- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -5,38 +5,51 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; -const CONSOLEAPI_CLASS_ID = "{b49c18f8-3379-4fc0-8c90-d7772c1a9ff3}"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TargetFactory", "resource:///modules/devtools/Target.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", + "resource://gre/modules/devtools/dbg-server.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient", + "resource://gre/modules/devtools/dbg-client.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", "resource://gre/modules/devtools/WebConsoleUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "webConsoleDefinition", + "resource:///modules/devtools/ToolDefinitions.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/commonjs/sdk/core/promise.js"); +XPCOMUtils.defineLazyModuleGetter(this, "ViewHelpers", + "resource:///modules/devtools/ViewHelpers.jsm"); + const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; let l10n = new WebConsoleUtils.l10n(STRINGS_URI); +const BROWSER_CONSOLE_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no"; + this.EXPORTED_SYMBOLS = ["HUDService"]; /////////////////////////////////////////////////////////////////////////// //// The HUD service function HUD_SERVICE() { this.hudReferences = {}; @@ -70,24 +83,49 @@ HUD_SERVICE.prototype = /** * Open a Web Console for the given target. * * @see devtools/framework/Target.jsm for details about targets. * * @param object aTarget * The target that the web console will connect to. - * @param nsIDOMElement aIframe - * The iframe element into which to place the web console. + * @param nsIDOMWindow aIframeWindow + * The window where the web console UI is already loaded. + * @param nsIDOMWindow aChromeWindow + * The window of the web console owner. * @return object * A Promise object for the opening of the new WebConsole instance. */ - openWebConsole: function HS_openWebConsole(aTarget, aIframe) + openWebConsole: + function HS_openWebConsole(aTarget, aIframeWindow, aChromeWindow) { - let hud = new WebConsole(aTarget, aIframe); + let hud = new WebConsole(aTarget, aIframeWindow, aChromeWindow); + this.hudReferences[hud.hudId] = hud; + return hud.init(); + }, + + /** + * Open a Browser Console for the given target. + * + * @see devtools/framework/Target.jsm for details about targets. + * + * @param object aTarget + * The target that the browser console will connect to. + * @param nsIDOMWindow aIframeWindow + * The window where the browser console UI is already loaded. + * @param nsIDOMWindow aChromeWindow + * The window of the browser console owner. + * @return object + * A Promise object for the opening of the new BrowserConsole instance. + */ + openBrowserConsole: + function HS_openBrowserConsole(aTarget, aIframeWindow, aChromeWindow) + { + let hud = new BrowserConsole(aTarget, aIframeWindow, aChromeWindow); this.hudReferences[hud.hudId] = hud; return hud.init(); }, /** * Returns the HeadsUpDisplay object associated to a content window. * * @param nsIDOMWindow aContentWindow @@ -139,46 +177,50 @@ HUD_SERVICE.prototype = }; /** * A WebConsole instance is an interactive console initialized *per target* * that displays console log data as well as provides an interactive terminal to * manipulate the target's document content. * - * This object only wraps the iframe that holds the Web Console UI. + * This object only wraps the iframe that holds the Web Console UI. This is + * meant to be an integration point between the Firefox UI and the Web Console + * UI and features. * * @constructor * @param object aTarget * The target that the web console will connect to. - * @param nsIDOMElement aIframe - * iframe into which we should create the WebConsole UI. + * @param nsIDOMWindow aIframeWindow + * The window where the web console UI is already loaded. + * @param nsIDOMWindow aChromeWindow + * The window of the web console owner. */ -function WebConsole(aTarget, aIframe) +function WebConsole(aTarget, aIframeWindow, aChromeWindow) { - this.iframe = aIframe; - this.iframe.className = "web-console-frame"; - this.chromeDocument = this.iframe.ownerDocument; - this.chromeWindow = this.chromeDocument.defaultView; + this.iframeWindow = aIframeWindow; + this.chromeWindow = aChromeWindow; this.hudId = "hud_" + Date.now(); this.target = aTarget; this.browserWindow = this.chromeWindow.top; + let element = this.browserWindow.document.documentElement; if (element.getAttribute("windowtype") != "navigator:browser") { this.browserWindow = HUDService.currentContext(); } } WebConsole.prototype = { + iframeWindow: null, chromeWindow: null, - chromeDocument: null, + browserWindow: null, hudId: null, target: null, - iframe: null, + _browserConsole: false, _destroyer: null, /** * Getter for HUDService.lastFinishedRequestCallback. * * @see HUDService.lastFinishedRequestCallback * @type function */ @@ -207,47 +249,18 @@ WebConsole.prototype = { /** * Initialize the Web Console instance. * * @return object * A Promise for the initialization. */ init: function WC_init() { - let deferred = Promise.defer(); - - let onIframeLoad = function() { - this.iframe.removeEventListener("load", onIframeLoad, true); - initUI(); - }.bind(this); - - let initUI = function() { - this.iframeWindow = this.iframe.contentWindow.wrappedJSObject; - this.ui = new this.iframeWindow.WebConsoleFrame(this); - this.ui.init().then(onSuccess, onFailure); - }.bind(this); - - let onSuccess = function() { - deferred.resolve(this); - }.bind(this); - - let onFailure = function(aReason) { - deferred.reject(aReason); - }; - - let win, doc; - if ((win = this.iframe.contentWindow) && - (doc = win.document) && - doc.readyState == "complete") { - initUI(); - } - else { - this.iframe.addEventListener("load", onIframeLoad, true); - } - return deferred.promise; + this.ui = new this.iframeWindow.WebConsoleFrame(this); + return this.ui.init().then(() => this); }, /** * Retrieve the Web Console panel title. * * @return string * The Web Console panel title. */ @@ -355,16 +368,20 @@ WebConsole.prototype = { */ viewSourceInDebugger: function WC_viewSourceInDebugger(aSourceURL, aSourceLine) { let self = this; let panelWin = null; let debuggerWasOpen = true; let toolbox = gDevTools.getToolbox(this.target); + if (!toolbox) { + self.viewSource(aSourceURL, aSourceLine); + return; + } if (!toolbox.getPanel("jsdebugger")) { debuggerWasOpen = false; let toolboxWin = toolbox.doc.defaultView; toolboxWin.addEventListener("Debugger:AfterSourcesAdded", function afterSourcesAdded() { toolboxWin.removeEventListener("Debugger:AfterSourcesAdded", afterSourcesAdded); @@ -400,16 +417,50 @@ WebConsole.prototype = { return; } panelWin.removeEventListener("Debugger:SourceShown", onSource, false); panelWin.DebuggerView.editor.setCaretPosition(aSourceLine - 1); } }, /** + * Retrieve information about the JavaScript debugger's stackframes list. This + * is used to allow the Web Console to evaluate code in the selected + * stackframe. + * + * @return object|null + * An object which holds: + * - frames: the active ThreadClient.cachedFrames array. + * - selected: depth/index of the selected stackframe in the debugger + * UI. + * If the debugger is not open or if it's not paused, then |null| is + * returned. + */ + getDebuggerFrames: function WC_getDebuggerFrames() + { + let toolbox = gDevTools.getToolbox(this.target); + if (!toolbox) { + return null; + } + let panel = toolbox.getPanel("jsdebugger"); + if (!panel) { + return null; + } + let framesController = panel.panelWin.gStackFrames; + let thread = framesController.activeThread; + if (thread && thread.paused) { + return { + frames: thread.cachedFrames, + selected: framesController.currentFrame, + }; + } + return null; + }, + + /** * Destroy the object. Call this method to avoid memory leaks when the Web * Console is closed. * * @return object * A Promise object that is resolved once the Web Console is closed. */ destroy: function WC_destroy() { @@ -447,21 +498,112 @@ WebConsole.prototype = { else { onDestroy(); } return this._destroyer.promise; }, }; + +/** + * A BrowserConsole instance is an interactive console initialized *per target* + * that displays console log data as well as provides an interactive terminal to + * manipulate the target's document content. + * + * This object only wraps the iframe that holds the Browser Console UI. This is + * meant to be an integration point between the Firefox UI and the Browser Console + * UI and features. + * + * @constructor + * @param object aTarget + * The target that the browser console will connect to. + * @param nsIDOMWindow aIframeWindow + * The window where the browser console UI is already loaded. + * @param nsIDOMWindow aChromeWindow + * The window of the browser console owner. + */ +function BrowserConsole() +{ + WebConsole.apply(this, arguments); +} + +ViewHelpers.create({ constructor: BrowserConsole, proto: WebConsole.prototype }, +{ + _browserConsole: true, + _bc_init: null, + _bc_destroyer: null, + + $init: WebConsole.prototype.init, + + /** + * Initialize the Browser Console instance. + * + * @return object + * A Promise for the initialization. + */ + init: function BC_init() + { + if (this._bc_init) { + return this._bc_init; + } + + let window = this.iframeWindow; + let onClose = () => { + window.removeEventListener("unload", onClose); + this.destroy(); + }; + window.addEventListener("unload", onClose); + + this._bc_init = this.$init().then((aReason) => { + let title = this.ui.rootElement.getAttribute("browserConsoleTitle"); + this.ui.rootElement.setAttribute("title", title); + return aReason; + }); + + return this._bc_init; + }, + + $destroy: WebConsole.prototype.destroy, + + /** + * Destroy the object. + * + * @return object + * A Promise object that is resolved once the Browser Console is closed. + */ + destroy: function BC_destroy() + { + if (this._bc_destroyer) { + return this._bc_destroyer.promise; + } + + this._bc_destroyer = Promise.defer(); + + let chromeWindow = this.chromeWindow; + this.$destroy().then(() => + this.target.client.close(() => { + HeadsUpDisplayUICommands._browserConsoleID = null; + chromeWindow.close(); + this._bc_destroyer.resolve(null); + })); + + return this._bc_destroyer.promise; + }, +}); + + ////////////////////////////////////////////////////////////////////////// // HeadsUpDisplayUICommands ////////////////////////////////////////////////////////////////////////// var HeadsUpDisplayUICommands = { + _browserConsoleID: null, + _browserConsoleDefer: null, + /** * Toggle the Web Console for the current tab. * * @return object * A Promise for either the opening of the toolbox that holds the Web * Console, or a Promise for the closing of the toolbox. */ toggleHUD: function UIC_toggleHUD() @@ -488,11 +630,88 @@ var HeadsUpDisplayUICommands = { if (!tab || !TargetFactory.isKnownTab(tab)) { return null; } let target = TargetFactory.forTab(tab); let toolbox = gDevTools.getToolbox(target); let panel = toolbox ? toolbox.getPanel("webconsole") : null; return panel ? panel.hud : null; }, + + /** + * Toggle the Browser Console. + */ + toggleBrowserConsole: function UIC_toggleBrowserConsole() + { + if (this._browserConsoleID) { + let hud = HUDService.getHudReferenceById(this._browserConsoleID); + return hud.destroy(); + } + + if (this._browserConsoleDefer) { + return this._browserConsoleDefer.promise; + } + + this._browserConsoleDefer = Promise.defer(); + + function connect() + { + let deferred = Promise.defer(); + + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + client.connect(() => + client.listTabs((aResponse) => + deferred.resolve({ form: aResponse, client: client }) + )); + + return deferred.promise; + } + + let target; + function getTarget(aConnection) + { + let options = { + form: aConnection.form, + client: aConnection.client, + chrome: true, + }; + + return TargetFactory.forRemoteTab(options); + } + + function openWindow(aTarget) + { + target = aTarget; + + let deferred = Promise.defer(); + + let win = Services.ww.openWindow(null, webConsoleDefinition.url, "_blank", + BROWSER_CONSOLE_WINDOW_FEATURES, null); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad); + deferred.resolve(win); + }); + + return deferred.promise; + } + + connect().then(getTarget).then(openWindow).then((aWindow) => + HUDService.openBrowserConsole(target, aWindow, aWindow) + .then((aBrowserConsole) => { + this._browserConsoleID = aBrowserConsole.hudId; + this._browserConsoleDefer.resolve(aBrowserConsole); + this._browserConsoleDefer = null; + })); + + return this._browserConsoleDefer.promise; + }, + + get browserConsole() { + return HUDService.getHudReferenceById(this._browserConsoleID); + }, }; const HUDService = new HUD_SERVICE();
--- a/browser/devtools/webconsole/WebConsolePanel.jsm +++ b/browser/devtools/webconsole/WebConsolePanel.jsm @@ -36,36 +36,62 @@ WebConsolePanel.prototype = { * * @return object * A Promise that is resolved when the Web Console completes opening. */ open: function WCP_open() { let parentDoc = this._toolbox.doc; let iframe = parentDoc.getElementById("toolbox-panel-iframe-webconsole"); - let promise; + iframe.className = "web-console-frame"; + + // Make sure the iframe content window is ready. + let deferredIframe = Promise.defer(); + let win, doc; + if ((win = iframe.contentWindow) && + (doc = win.document) && + doc.readyState == "complete") { + deferredIframe.resolve(null); + } + else { + iframe.addEventListener("load", function onIframeLoad() { + iframe.removeEventListener("load", onIframeLoad, true); + deferredIframe.resolve(null); + }, true); + } // Local debugging needs to make the target remote. + let promiseTarget; if (!this.target.isRemote) { - promise = this.target.makeRemote(); - } else { - promise = Promise.resolve(this.target); + promiseTarget = this.target.makeRemote(); + } + else { + promiseTarget = Promise.resolve(this.target); } - return promise - .then(function(aTarget) { + // 1. Wait for the iframe to load. + // 2. Wait for the remote target. + // 3. Open the Web Console. + return deferredIframe.promise + .then(() => promiseTarget) + .then((aTarget) => { this._frameWindow._remoteTarget = aTarget; - return HUDService.openWebConsole(this.target, iframe); - }.bind(this)) - .then(function onSuccess(aWebConsole) { + + let webConsoleUIWindow = iframe.contentWindow.wrappedJSObject; + let chromeWindow = iframe.ownerDocument.defaultView; + + return HUDService.openWebConsole(this.target, webConsoleUIWindow, + chromeWindow); + }) + .then((aWebConsole) => { this.hud = aWebConsole; this._isReady = true; this.emit("ready"); return this; - }.bind(this), function onError(aReason) { + }, (aReason) => { let msg = "WebConsolePanel open failed. " + aReason.error + ": " + aReason.message; dump(msg + "\n"); Cu.reportError(msg); }); }, get target() this._toolbox.target, @@ -75,15 +101,13 @@ WebConsolePanel.prototype = { destroy: function WCP_destroy() { if (this._destroyer) { return this._destroyer; } this._destroyer = this.hud.destroy(); - this._destroyer.then(function() { - this.emit("destroyed"); - }.bind(this)); + this._destroyer.then(() => this.emit("destroyed")); return this._destroyer; }, };
--- a/browser/devtools/webconsole/test/Makefile.in +++ b/browser/devtools/webconsole/test/Makefile.in @@ -37,17 +37,16 @@ MOCHITEST_BROWSER_FILES = \ browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js \ browser_webconsole_bug_586388_select_all.js \ browser_webconsole_bug_588967_input_expansion.js \ browser_webconsole_log_node_classes.js \ browser_webconsole_network_panel.js \ browser_webconsole_jsterm.js \ browser_webconsole_null_and_undefined_output.js \ browser_webconsole_output_order.js \ - browser_webconsole_property_panel.js \ browser_webconsole_property_provider.js \ browser_webconsole_bug_587617_output_copy.js \ browser_webconsole_bug_585237_line_limit.js \ browser_webconsole_bug_582201_duplicate_errors.js \ browser_webconsole_bug_580454_timestamp_l10n.js \ browser_webconsole_netlogging.js \ browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js \ browser_webconsole_bug_594477_clickable_output.js \ @@ -111,20 +110,22 @@ MOCHITEST_BROWSER_FILES = \ browser_result_format_as_string.js \ browser_webconsole_bug_737873_mixedcontent.js \ browser_output_breaks_after_console_dir_uninspectable.js \ browser_console_log_inspectable_object.js \ browser_bug_638949_copy_link_location.js \ browser_output_longstring_expand.js \ browser_netpanel_longstring_expand.js \ browser_repeated_messages_accuracy.js \ + browser_webconsole_bug_821877_csp_errors.js \ + browser_eval_in_debugger_stackframe.js \ + browser_console_variables_view.js \ + browser_console_variables_view_while_debugging.js \ + browser_console.js \ head.js \ - browser_webconsole_bug_821877_csp_errors.js \ - test-bug-821877-csperrors.html \ - test-bug-821877-csperrors.html^headers^ \ $(NULL) ifeq ($(OS_ARCH), Darwin) MOCHITEST_BROWSER_FILES += \ browser_webconsole_bug_804845_ctrl_key_nav.js \ $(NULL) endif @@ -212,11 +213,14 @@ MOCHITEST_BROWSER_FILES += \ test_bug_770099_bad_policy_uri.html \ test_bug_770099_bad_policy_uri.html^headers^ \ test-result-format-as-string.html \ test-bug-737873-mixedcontent.html \ test-repeated-messages.html \ test-bug-766001-console-log.js \ test-bug-766001-js-console-links.html \ test-bug-766001-js-errors.js \ + test-bug-821877-csperrors.html \ + test-bug-821877-csperrors.html^headers^ \ + test-eval-in-stackframe.html \ $(NULL) include $(topsrcdir)/config/rules.mk
--- a/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js +++ b/browser/devtools/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js @@ -47,17 +47,17 @@ function test() "jsterm input is also displayed"); is(hud.outputNode.textContent.indexOf("Permission denied"), -1, "no permission denied errors"); gBrowser.selectedBrowser.addEventListener("load", onPageLoad2, true); content.location = TEST_URI2; }, - failureFn: finishTest, + failureFn: finishTestWithError, }; function onPageLoad2() { gBrowser.selectedBrowser.removeEventListener("load", onPageLoad2, true); hud.jsterm.clearOutput(); hud.jsterm.execute("window.location.href"); @@ -77,33 +77,33 @@ function test() isnot(node.textContent.indexOf("window.location.href"), -1, "jsterm input is also displayed"); is(hud.outputNode.textContent.indexOf("Permission denied"), -1, "no permission denied errors"); gBrowser.goBack(); waitForSuccess(waitForBack); }, - failureFn: finishTest, + failureFn: finishTestWithError, }; let waitForBack = { name: "go back", validatorFn: function() { return content.location.href == TEST_URI1; }, successFn: function() { hud.jsterm.clearOutput(); hud.jsterm.execute("window.location.href"); waitForSuccess(waitForLocation3); }, - failureFn: finishTest, + failureFn: finishTestWithError, }; let waitForLocation3 = { name: "window.location.href result is displayed after goBack()", validatorFn: function() { let node = hud.outputNode.getElementsByClassName("webconsole-msg-output")[0]; return node && node.textContent.indexOf(TEST_URI1) > -1; @@ -113,11 +113,17 @@ function test() let node = hud.outputNode.getElementsByClassName("webconsole-msg-input")[0]; isnot(node.textContent.indexOf("window.location.href"), -1, "jsterm input is also displayed"); is(hud.outputNode.textContent.indexOf("Permission denied"), -1, "no permission denied errors"); executeSoon(finishTest); }, - failureFn: finishTest, + failureFn: finishTestWithError, }; + + function finishTestWithError() + { + info("output content: " + hud.outputNode.textContent); + finishTest(); + } }
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console.js @@ -0,0 +1,93 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test the basic features of the Browser Console, bug 587757. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html"; + +function test() +{ + HUDConsoleUI.toggleBrowserConsole().then(consoleOpened); +} + +function consoleOpened(hud) +{ + hud.jsterm.clearOutput(true); + + expectUncaughtException(); + executeSoon(() => { + foobarExceptionBug587757(); + }); + + // Add a message from a chrome window. + hud.iframeWindow.console.log("bug587757a"); + + // Add a message from a content window. + content.console.log("bug587757b"); + + // Test eval. + hud.jsterm.execute("document.location.href"); + + // Check for network requests. + let xhr = new XMLHttpRequest(); + xhr.onload = () => info("xhr loaded, status is: " + xhr.status); + xhr.open("get", TEST_URI, true); + xhr.send(); + + let chromeConsole = -1; + let contentConsole = -1; + let execValue = -1; + let exception = -1; + let xhrRequest = false; + + let output = hud.outputNode; + function performChecks() + { + let text = output.textContent; + chromeConsole = text.indexOf("bug587757a"); + contentConsole = text.indexOf("bug587757b"); + execValue = text.indexOf("browser.xul"); + exception = text.indexOf("foobarExceptionBug587757"); + + xhrRequest = false; + let urls = output.querySelectorAll(".webconsole-msg-url"); + for (let url of urls) { + if (url.value.indexOf(TEST_URI) > -1) { + xhrRequest = true; + break; + } + } + } + + function showResults() + { + isnot(chromeConsole, -1, "chrome window console.log() is displayed"); + isnot(contentConsole, -1, "content window console.log() is displayed"); + isnot(execValue, -1, "jsterm eval result is displayed"); + isnot(exception, -1, "exception is displayed"); + ok(xhrRequest, "xhr request is displayed"); + } + + waitForSuccess({ + name: "messages displayed", + validatorFn: () => { + performChecks(); + return chromeConsole > -1 && + contentConsole > -1 && + execValue > -1 && + exception > -1 && + xhrRequest; + }, + successFn: () => { + showResults(); + executeSoon(finishTest); + }, + failureFn: () => { + showResults(); + info("output: " + output.textContent); + executeSoon(finishTest); + }, + }); +}
--- a/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js +++ b/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js @@ -31,27 +31,28 @@ function performTest(hud) }, successFn: function() { isnot(hud.outputNode.textContent.indexOf("myObj = {"), -1, "myObj = ... is shown"); let clickable = hud.outputNode.querySelector(".hud-clickable"); ok(clickable, "the console.log() object .hud-clickable was found"); - isnot(clickable.textContent.indexOf("omgBug676722"), -1, + isnot(clickable.textContent.indexOf("Object"), -1, "clickable node content is correct"); - document.addEventListener("popupshown", function _onPopupShown(aEvent) { - document.removeEventListener("popupshown", _onPopupShown); + hud.jsterm.once("variablesview-fetched", + (aEvent, aVar) => { + ok(aVar, "object inspector opened on click"); - isnot(aEvent.target.label.indexOf("omgBug676722"), -1, - "object inspector opened on click"); - - executeSoon(finishTest); - }); + findVariableViewProperties(aVar, [{ + name: "abba", + value: "omgBug676722", + }], { webconsole: hud }).then(finishTest); + }); executeSoon(function() { EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow); }); }, failureFn: finishTest, }); }
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_variables_view.js @@ -0,0 +1,178 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that makes sure web console eval happens in the user-selected stackframe +// from the js debugger. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html"; + +let gWebConsole, gJSTerm, gVariablesView; + +function test() +{ + addTab(TEST_URI); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, consoleOpened); + }, true); +} + +function consoleOpened(hud) +{ + gWebConsole = hud; + gJSTerm = hud.jsterm; + gJSTerm.execute("fooObj", onExecuteFooObj); +} + +function onExecuteFooObj() +{ + let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output"); + ok(msg, "output message found"); + isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check"); + + gJSTerm.once("variablesview-fetched", onFooObjFetch); + + executeSoon(() => + EventUtils.synthesizeMouse(msg, 2, 2, {}, gWebConsole.iframeWindow) + ); +} + +function onFooObjFetch(aEvent, aVar) +{ + gVariablesView = aVar._variablesView; + ok(gVariablesView, "variables view object"); + + findVariableViewProperties(aVar, [ + { name: "testProp", value: "testValue" }, + ], { webconsole: gWebConsole }).then(onTestPropFound); +} + +function onTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the |testProp| property in the variables view"); + + is(content.wrappedJSObject.fooObj.testProp, aResults[0].value, + "|fooObj.testProp| value is correct"); + + // Check that property value updates work and that jsterm functions can be + // used. + updateVariablesViewProperty({ + property: prop, + field: "value", + string: "document.title + window.location + $('p')", + webconsole: gWebConsole, + callback: onFooObjFetchAfterUpdate, + }); +} + +function onFooObjFetchAfterUpdate(aEvent, aVar) +{ + info("onFooObjFetchAfterUpdate"); + let para = content.document.querySelector("p"); + let expectedValue = content.document.title + content.location + para; + + findVariableViewProperties(aVar, [ + { name: "testProp", value: expectedValue }, + ], { webconsole: gWebConsole }).then(onUpdatedTestPropFound); +} + +function onUpdatedTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the updated |testProp| property value"); + + is(content.wrappedJSObject.fooObj.testProp, aResults[0].value, + "|fooObj.testProp| value has been updated"); + + // Check that property name updates work. + updateVariablesViewProperty({ + property: prop, + field: "name", + string: "testUpdatedProp", + webconsole: gWebConsole, + callback: onFooObjFetchAfterPropRename, + }); +} + +function onFooObjFetchAfterPropRename(aEvent, aVar) +{ + info("onFooObjFetchAfterPropRename"); + + let para = content.document.querySelector("p"); + let expectedValue = content.document.title + content.location + para; + + // Check that the new value is in the variables view. + findVariableViewProperties(aVar, [ + { name: "testUpdatedProp", value: expectedValue }, + ], { webconsole: gWebConsole }).then(onRenamedTestPropFound); +} + +function onRenamedTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the renamed |testProp| property"); + + ok(!content.wrappedJSObject.fooObj.testProp, + "|fooObj.testProp| has been deleted"); + is(content.wrappedJSObject.fooObj.testUpdatedProp, aResults[0].value, + "|fooObj.testUpdatedProp| is correct"); + + // Check that property value updates that cause exceptions are reported in + // the web console output. + updateVariablesViewProperty({ + property: prop, + field: "value", + string: "foobarzFailure()", + webconsole: gWebConsole, + callback: onPropUpdateError, + }); +} + +function onPropUpdateError(aEvent, aVar) +{ + info("onPropUpdateError"); + + let para = content.document.querySelector("p"); + let expectedValue = content.document.title + content.location + para; + + // Make sure the property did not change. + findVariableViewProperties(aVar, [ + { name: "testUpdatedProp", value: expectedValue }, + ], { webconsole: gWebConsole }).then(onRenamedTestPropFoundAgain); +} + +function onRenamedTestPropFoundAgain(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the renamed |testProp| property again"); + + let outputNode = gWebConsole.outputNode; + + waitForSuccess({ + name: "exception in property update reported in the web console output", + validatorFn: () => outputNode.textContent.indexOf("foobarzFailure") != -1, + successFn: testPropDelete.bind(null, prop), + failureFn: testPropDelete.bind(null, prop), + }); +} + +function testPropDelete(aProp) +{ + gVariablesView.window.focus(); + aProp.focus(); + + executeSoon(() => { + EventUtils.synthesizeKey("VK_DELETE", {}, gVariablesView.window); + gWebConsole = gJSTerm = gVariablesView = null; + }); + + waitForSuccess({ + name: "property deleted", + validatorFn: () => !("testUpdatedProp" in content.wrappedJSObject.fooObj), + successFn: finishTest, + failureFn: finishTest, + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_variables_view_while_debugging.js @@ -0,0 +1,132 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that makes sure web console eval happens in the user-selected stackframe +// from the js debugger, when changing the value of a property in the variables +// view. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html"; + +let gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController, + gStackframes, gVariablesView; + +function test() +{ + addTab(TEST_URI); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, consoleOpened); + }, true); +} + +function consoleOpened(hud) +{ + gWebConsole = hud; + gJSTerm = hud.jsterm; + + executeSoon(() => { + info("openDebugger"); + openDebugger().then(debuggerOpened); + }); +} + +function debuggerOpened(aResult) +{ + gDebuggerWin = aResult.panelWin; + gDebuggerController = gDebuggerWin.DebuggerController; + gThread = gDebuggerController.activeThread; + gStackframes = gDebuggerController.StackFrames; + + executeSoon(() => { + gThread.addOneTimeListener("framesadded", onFramesAdded); + + info("firstCall()"); + content.wrappedJSObject.firstCall(); + }); +} + +function onFramesAdded() +{ + info("onFramesAdded"); + + executeSoon(() => + openConsole(null, () => + gJSTerm.execute("fooObj", onExecuteFooObj) + ) + ); +} + + +function onExecuteFooObj() +{ + let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output"); + ok(msg, "output message found"); + isnot(msg.textContent.indexOf("[object Object]"), -1, "message text check"); + + gJSTerm.once("variablesview-fetched", onFooObjFetch); + + executeSoon(() => EventUtils.synthesizeMouse(msg, 2, 2, {}, + gWebConsole.iframeWindow)); +} + +function onFooObjFetch(aEvent, aVar) +{ + gVariablesView = aVar._variablesView; + ok(gVariablesView, "variables view object"); + + findVariableViewProperties(aVar, [ + { name: "testProp2", value: "testValue2" }, + { name: "testProp", value: "testValue", dontMatch: true }, + ], { webconsole: gWebConsole }).then(onTestPropFound); +} + +function onTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the |testProp2| property in the variables view"); + + // Check that property value updates work and that jsterm functions can be + // used. + updateVariablesViewProperty({ + property: prop, + field: "value", + string: "document.title + foo2 + $('p')", + webconsole: gWebConsole, + callback: onFooObjFetchAfterUpdate, + }); +} + +function onFooObjFetchAfterUpdate(aEvent, aVar) +{ + info("onFooObjFetchAfterUpdate"); + let para = content.document.querySelector("p"); + let expectedValue = content.document.title + "foo2SecondCall" + para; + + findVariableViewProperties(aVar, [ + { name: "testProp2", value: expectedValue }, + ], { webconsole: gWebConsole }).then(onUpdatedTestPropFound); +} + +function onUpdatedTestPropFound(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the updated |testProp2| property value"); + + // Check that testProp2 was updated. + executeSoon(() => gJSTerm.execute("fooObj.testProp2", onExecuteFooObjTestProp2)); +} + +function onExecuteFooObjTestProp2() +{ + let para = content.document.querySelector("p"); + let expected = content.document.title + "foo2SecondCall" + para; + + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "fooObj.testProp2 is correct"); + + gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController = + gStackframes = gVariablesView = null; + executeSoon(finishTest); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_eval_in_debugger_stackframe.js @@ -0,0 +1,150 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that makes sure web console eval happens in the user-selected stackframe +// from the js debugger. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-eval-in-stackframe.html"; + +let gWebConsole, gJSTerm, gDebuggerWin, gThread, gDebuggerController, gStackframes; + +function test() +{ + addTab(TEST_URI); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, consoleOpened); + }, true); +} + +function consoleOpened(hud) +{ + gWebConsole = hud; + gJSTerm = hud.jsterm; + gJSTerm.execute("foo", onExecuteFoo); +} + +function onExecuteFoo() +{ + isnot(gWebConsole.outputNode.textContent.indexOf("globalFooBug783499"), -1, + "|foo| value is correct"); + + gJSTerm.clearOutput(); + + // Test for Bug 690529 - Web Console and Scratchpad should evaluate + // expressions in the scope of the content window, not in a sandbox. + executeSoon(() => gJSTerm.execute("foo2 = 'newFoo'; window.foo2", onNewFoo2)); +} + +function onNewFoo2() +{ + is(gWebConsole.outputNode.textContent.indexOf("undefined"), -1, + "|undefined| is not displayed after adding |foo2|"); + + let msg = gWebConsole.outputNode.querySelector(".webconsole-msg-output"); + ok(msg, "output result found"); + + isnot(msg.textContent.indexOf("newFoo"), -1, + "'newFoo' is displayed after adding |foo2|"); + + gJSTerm.clearOutput(); + + info("openDebugger"); + executeSoon(() => openDebugger().then(debuggerOpened)); +} + +function debuggerOpened(aResult) +{ + gDebuggerWin = aResult.panelWin; + gDebuggerController = gDebuggerWin.DebuggerController; + gThread = gDebuggerController.activeThread; + gStackframes = gDebuggerController.StackFrames; + + info("openConsole"); + executeSoon(() => + openConsole(null, () => + gJSTerm.execute("foo + foo2", onExecuteFooAndFoo2) + ) + ); +} + +function onExecuteFooAndFoo2() +{ + let expected = "globalFooBug783499newFoo"; + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "|foo + foo2| is displayed after starting the debugger"); + + executeSoon(() => { + gJSTerm.clearOutput(); + + info("openDebugger"); + openDebugger().then(() => { + gThread.addOneTimeListener("framesadded", onFramesAdded); + + info("firstCall()"); + content.wrappedJSObject.firstCall(); + }); + }); +} + +function onFramesAdded() +{ + info("onFramesAdded, openConsole() now"); + executeSoon(() => + openConsole(null, () => + gJSTerm.execute("foo + foo2", onExecuteFooAndFoo2InSecondCall) + ) + ); +} + +function onExecuteFooAndFoo2InSecondCall() +{ + let expected = "globalFooBug783499foo2SecondCall"; + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "|foo + foo2| from |secondCall()|"); + + executeSoon(() => { + gJSTerm.clearOutput(); + + info("openDebugger and selectFrame(1)"); + + openDebugger().then(() => { + gStackframes.selectFrame(1); + + info("openConsole"); + executeSoon(() => + openConsole(null, () => + gJSTerm.execute("foo + foo2 + foo3", onExecuteFoo23InFirstCall) + ) + ); + }); + }); +} + +function onExecuteFoo23InFirstCall() +{ + let expected = "fooFirstCallnewFoofoo3FirstCall"; + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "|foo + foo2 + foo3| from |firstCall()|"); + + executeSoon(() => + gJSTerm.execute("foo = 'abba'; foo3 = 'bug783499'; foo + foo3", + onExecuteFooAndFoo3ChangesInFirstCall)); +} + +function onExecuteFooAndFoo3ChangesInFirstCall() +{ + let expected = "abbabug783499"; + isnot(gWebConsole.outputNode.textContent.indexOf(expected), -1, + "|foo + foo3| updated in |firstCall()|"); + + is(content.wrappedJSObject.foo, "globalFooBug783499", "|foo| in content window"); + is(content.wrappedJSObject.foo2, "newFoo", "|foo2| in content window"); + ok(!content.wrappedJSObject.foo3, "|foo3| was not added to the content window"); + + gWebConsole = gJSTerm = gDebuggerWin = gThread = gDebuggerController = + gStackframes = null; + executeSoon(finishTest); +}
--- a/browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js +++ b/browser/devtools/webconsole/test/browser_output_breaks_after_console_dir_uninspectable.js @@ -16,23 +16,24 @@ function test() openConsole(null, performTest); }, true); } function performTest(hud) { hud.jsterm.clearOutput(true); - content.console.log("fooBug773466a"); - content.console.dir(function funBug773466(){}); + hud.jsterm.execute("console.log('fooBug773466a')"); + hud.jsterm.execute("myObj = Object.create(null)"); + hud.jsterm.execute("console.dir(myObj)"); waitForSuccess({ name: "eval results are shown", validatorFn: function() { - return hud.outputNode.textContent.indexOf("funBug773466") > -1; + return hud.outputNode.querySelector(".webconsole-msg-inspector"); }, successFn: function() { isnot(hud.outputNode.textContent.indexOf("fooBug773466a"), -1, "fooBug773466a shows"); ok(hud.outputNode.querySelector(".webconsole-msg-inspector"), "the console.dir() tree shows");
--- a/browser/devtools/webconsole/test/browser_result_format_as_string.js +++ b/browser/devtools/webconsole/test/browser_result_format_as_string.js @@ -32,18 +32,18 @@ function performTest(hud) successFn: function() { is(hud.outputNode.textContent.indexOf("bug772506_content"), -1, "no content element found"); ok(!hud.outputNode.querySelector("div"), "no div element found"); let msg = hud.outputNode.querySelector(".webconsole-msg-output"); ok(msg, "eval output node found"); - isnot(msg.textContent.indexOf("HTMLDivElement"), -1, - "HTMLDivElement string found"); + is(msg.textContent.indexOf("HTMLDivElement"), -1, + "HTMLDivElement string not displayed"); EventUtils.synthesizeMouseAtCenter(msg, {type: "mousemove"}); ok(!gBrowser._bug772506, "no content variable"); finishTest(); }, failureFn: finishTest, }); }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js @@ -44,16 +44,18 @@ function tabLoad2(aEvent) { }, failureFn: finishTest, }); } function networkPanelShown(aEvent) { document.removeEventListener(aEvent.type, networkPanelShown, false); + info("networkPanelShown"); + document.addEventListener("popupshown", networkPanelShowFailure, false); // The network panel should not open for the second time. EventUtils.sendMouseEvent({type: "mousedown"}, outputItem); EventUtils.sendMouseEvent({type: "click"}, outputItem); executeSoon(function() { aEvent.target.addEventListener("popuphidden", networkPanelHidden, false); @@ -65,16 +67,18 @@ function networkPanelShowFailure(aEvent) document.removeEventListener(aEvent.type, networkPanelShowFailure, false); ok(false, "the network panel should not show"); } function networkPanelHidden(aEvent) { this.removeEventListener(aEvent.type, networkPanelHidden, false); + info("networkPanelHidden"); + // The network panel should not show because this is a mouse event that starts // in a position and ends in another. EventUtils.sendMouseEvent({type: "mousedown", clientX: 3, clientY: 4}, outputItem); EventUtils.sendMouseEvent({type: "click", clientX: 5, clientY: 6}, outputItem); // The network panel should not show because this is a middle-click. @@ -89,83 +93,35 @@ function networkPanelHidden(aEvent) { EventUtils.sendMouseEvent({type: "click", button: 2}, outputItem); executeSoon(function() { document.removeEventListener("popupshown", networkPanelShowFailure, false); // Done with the network output. Now test the jsterm output and the property // panel. - HUD.jsterm.setInputValue("document"); - HUD.jsterm.execute(); + HUD.jsterm.execute("document", () => { + info("jsterm execute 'document' callback"); - waitForSuccess({ - name: "jsterm output message", - validatorFn: function() - { - return outputNode.querySelector(".webconsole-msg-output .hud-clickable"); - }, - successFn: function() - { - document.addEventListener("popupshown", propertyPanelShown, false); + HUD.jsterm.once("variablesview-open", onVariablesViewOpen); + let outputItem = outputNode + .querySelector(".webconsole-msg-output .hud-clickable"); + ok(outputItem, "jsterm output message found"); - // Send the mousedown and click events such that the property panel opens. - EventUtils.sendMouseEvent({type: "mousedown"}, outputItem); - EventUtils.sendMouseEvent({type: "click"}, outputItem); - }, - failureFn: finishTest, + // Send the mousedown and click events such that the property panel opens. + EventUtils.sendMouseEvent({type: "mousedown"}, outputItem); + EventUtils.sendMouseEvent({type: "click"}, outputItem); }); }); } -function propertyPanelShown(aEvent) { - document.removeEventListener(aEvent.type, propertyPanelShown, false); - - document.addEventListener("popupshown", propertyPanelShowFailure, false); - - // The property panel should not open for the second time. - EventUtils.sendMouseEvent({type: "mousedown"}, outputItem); - EventUtils.sendMouseEvent({type: "click"}, outputItem); +function onVariablesViewOpen() { + info("onVariablesViewOpen"); executeSoon(function() { - aEvent.target.addEventListener("popuphidden", propertyPanelHidden, false); - aEvent.target.hidePopup(); - }); -} - -function propertyPanelShowFailure(aEvent) { - document.removeEventListener(aEvent.type, propertyPanelShowFailure, false); - - ok(false, "the property panel should not show"); -} - -function propertyPanelHidden(aEvent) { - this.removeEventListener(aEvent.type, propertyPanelHidden, false); - - // The property panel should not show because this is a mouse event that - // starts in a position and ends in another. - EventUtils.sendMouseEvent({type: "mousedown", clientX: 3, clientY: 4}, - outputItem); - EventUtils.sendMouseEvent({type: "click", clientX: 5, clientY: 6}, - outputItem); - - // The property panel should not show because this is a middle-click. - EventUtils.sendMouseEvent({type: "mousedown", button: 1}, - outputItem); - EventUtils.sendMouseEvent({type: "click", button: 1}, - outputItem); - - // The property panel should not show because this is a right-click. - EventUtils.sendMouseEvent({type: "mousedown", button: 2}, - outputItem); - EventUtils.sendMouseEvent({type: "click", button: 2}, - outputItem); - - executeSoon(function() { - document.removeEventListener("popupshown", propertyPanelShowFailure, false); HUD = outputItem = null; executeSoon(finishTest); }); } function test() { addTab(TEST_URI); browser.addEventListener("load", function onLoad() {
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js @@ -14,16 +14,18 @@ const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 595350"; let win1 = window, win2; let openTabs = []; let loadedTabCount = 0; function test() { + requestLongerTimeout(2); + // Add two tabs in the main window. addTabs(win1); // Open a new window. win2 = OpenBrowserWindow(); win2.addEventListener("load", onWindowLoad, true); } @@ -59,17 +61,17 @@ function openConsoles() { ok(hud, "HUD is open for tab " + index); let window = hud.target.tab.linkedBrowser.contentWindow; window.console.log("message for tab " + index); consolesOpen++; }.bind(null, i)); } waitForSuccess({ - timeout: 10000, + timeout: 15000, name: "4 web consoles opened", validatorFn: function() { return consolesOpen == 4; }, successFn: closeConsoles, failureFn: closeConsoles, });
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js @@ -19,21 +19,21 @@ let tempScope = {}; Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope); let longString = (new Array(tempScope.DebuggerServer.LONG_STRING_LENGTH + 4)).join("a"); let initialString = longString.substring(0, tempScope.DebuggerServer.LONG_STRING_INITIAL_LENGTH); let inputValues = [ // [showsPropertyPanel?, input value, expected output format, - // print() output, console output, optional console API test] + // print() output, console API output, optional console API test] // 0 [false, "'hello \\nfrom \\rthe \\\"string world!'", - '"hello \\nfrom \\rthe \\"string world!"', + '"hello \nfrom \rthe "string world!"', "hello \nfrom \rthe \"string world!"], // 1 [false, "'\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165'", "\"\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165\"", "\xFA\u1E47\u0129\xE7\xF6d\xEA \u021B\u0115\u0219\u0165"], // 2 @@ -47,58 +47,52 @@ let inputValues = [ // 5 [false, "42", "42"], // 6 [false, "'42'", '"42"', "42"], // 7 - [false, "/foobar/", "/foobar/"], + [true, "/foobar/", "[object RegExp]", '"/foobar/"', "[object RegExp]"], // 8 [false, "null", "null"], // 9 [false, "undefined", "undefined"], // 10 [false, "true", "true"], // 11 - [false, "document.getElementById", "function getElementById() {\n [native code]\n}", + [true, "document.getElementById", "[object Function]", "function getElementById() {\n [native code]\n}", - "function getElementById() {\n [native code]\n}", - "document.wrappedJSObject.getElementById"], + "[object Function]"], // 12 - [false, "(function() { return 42; })", "function () { return 42; }", - "function () { return 42; }", - "(function () { return 42; })"], + [true, "(function() { return 42; })", "[object Function]", + "function () { return 42; }", "[object Function]"], // 13 - [false, "new Date(" + dateNow + ")", (new Date(dateNow)).toString()], + [true, "new Date(" + dateNow + ")", "[object Date]", (new Date(dateNow)).toString(), "[object Date]"], // 14 - [true, "document.body", "[object HTMLBodyElement", "[object HTMLBodyElement", - "[object HTMLBodyElement", - "document.wrappedJSObject.body"], + [true, "document.body", "[object HTMLBodyElement]"], // 15 - [true, "window.location", TEST_URI], + [true, "window.location", "[object Location]", TEST_URI, "[object Location]"], // 16 - [true, "[1,2,3,'a','b','c','4','5']", '[1, 2, 3, "a", "b", "c", "4", "5"]', + [true, "[1,2,3,'a','b','c','4','5']", '[object Array]', '1,2,3,a,b,c,4,5', - '[1, 2, 3, "a", "b", "c", "4", "5"]'], + "[object Array]"], // 17 - [true, "({a:'b', c:'d', e:1, f:'2'})", '({a:"b", c:"d", e:1, f:"2"})', - "[object Object", - '({a:"b", c:"d", e:1, f:"2"})'], + [true, "({a:'b', c:'d', e:1, f:'2'})", "[object Object]"], // 18 [false, "'" + longString + "'", '"' + initialString + "\"[\u2026]", initialString], ]; longString = null; initialString = null; @@ -149,20 +143,17 @@ function testGen() { inputValues[cpos][4] : printOutput; let consoleTest = inputValues[cpos][5] || inputValue; HUD.jsterm.clearOutput(); // Test the console.log() output. - // Ugly but it does the job. - with (content) { - eval("content.console.log(" + consoleTest + ")"); - } + HUD.jsterm.execute("console.log(" + consoleTest + ")"); waitForSuccess({ name: "console.log message for test #" + cpos, validatorFn: function() { return HUD.outputNode.querySelector(".hud-log"); }, successFn: subtestNext, @@ -228,38 +219,38 @@ function testGen() { "jsterm output is correct for inputValues[" + cpos + "]"); let messageBody = outputItem.querySelector(".webconsole-msg-body"); ok(messageBody, "we have the message body for inputValues[" + cpos + "]"); // Test click on output. let eventHandlerID = eventHandlers.length + 1; - let propertyPanelShown = function(aEvent) { - let label = aEvent.target.getAttribute("label"); - if (!label || label.indexOf(inputValue) == -1) { + let propertyPanelShown = function(aEvent, aView, aOptions) { + if (aOptions.label.indexOf(expectedOutput) == -1) { return; } - document.removeEventListener(aEvent.type, propertyPanelShown, false); + HUD.jsterm.off("variablesview-open", propertyPanelShown); + eventHandlers[eventHandlerID] = null; ok(showsPropertyPanel, "the property panel shown for inputValues[" + cpos + "]"); - aEvent.target.hidePopup(); + HUD.jsterm._splitter.state = "collapsed"; popupShown[cpos] = true; if (showsPropertyPanel) { - subtestNext(); + executeSoon(subtestNext); } }; - document.addEventListener("popupshown", propertyPanelShown, false); + HUD.jsterm.on("variablesview-open", propertyPanelShown); eventHandlers.push(propertyPanelShown); // Send the mousedown, mouseup and click events to check if the property // panel opens. EventUtils.sendMouseEvent({ type: "mousedown" }, messageBody, window); EventUtils.sendMouseEvent({ type: "click" }, messageBody, window); @@ -276,27 +267,28 @@ function testEnd() { if (testEnded) { return; } testEnded = true; for (let i = 0; i < eventHandlers.length; i++) { if (eventHandlers[i]) { - document.removeEventListener("popupshown", eventHandlers[i], false); + HUD.jsterm.off("variablesview-open", eventHandlers[i]); } } for (let i = 0; i < inputValues.length; i++) { if (inputValues[i][0] && !popupShown[i]) { ok(false, "the property panel failed to show for inputValues[" + i + "]"); } } HUD = inputValues = testDriver = null; executeSoon(finishTest); } function test() { + requestLongerTimeout(2); addTab(TEST_URI); browser.addEventListener("load", tabLoad, true); }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js @@ -8,31 +8,34 @@ * * ***** END LICENSE BLOCK ***** */ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-600183-charset.html"; function performTest(lastFinishedRequest, aConsole) { ok(lastFinishedRequest, "charset test page was loaded and logged"); + HUDService.lastFinishedRequestCallback = null; - aConsole.webConsoleClient.getResponseContent(lastFinishedRequest.actor, - function (aResponse) { - ok(!aResponse.contentDiscarded, "response body was not discarded"); - - let body = aResponse.content.text; - ok(body, "we have the response body"); + executeSoon(() => { + aConsole.webConsoleClient.getResponseContent(lastFinishedRequest.actor, + (aResponse) => { + ok(!aResponse.contentDiscarded, "response body was not discarded"); - let chars = "\u7684\u95ee\u5019!"; // 的问候! - isnot(body.indexOf("<p>" + chars + "</p>"), -1, - "found the chinese simplified string"); - executeSoon(finishTest); - }); + let body = aResponse.content.text; + ok(body, "we have the response body"); - HUDService.lastFinishedRequestCallback = null; + let chars = "\u7684\u95ee\u5019!"; // 的问候! + isnot(body.indexOf("<p>" + chars + "</p>"), -1, + "found the chinese simplified string"); + + HUDService.lastFinishedRequestCallback = null; + executeSoon(finishTest); + }); + }); } function test() { addTab("data:text/html;charset=utf-8,Web Console - bug 600183 test"); browser.addEventListener("load", function onLoad() { browser.removeEventListener("load", onLoad, true);
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601177_log_levels.js @@ -30,17 +30,17 @@ function performTest() "found exception"); findEntry(HUD, "hud-jswarn", "undefinedPropertyBug601177", "found strict warning"); findEntry(HUD, "hud-jswarn", "foobarBug601177strictError", "found strict error"); - finishTest(); + executeSoon(finishTest); } function findEntry(aHUD, aClass, aString, aMessage) { return testLogEntry(aHUD.outputNode, aString, aMessage, false, false, aClass); }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js @@ -15,28 +15,26 @@ function test() // open tab 1 addTab("data:text/html;charset=utf-8,Web Console test for bug 602572: log bodies checkbox. tab 1"); tabs.push(tab); browser.addEventListener("load", function onLoad1(aEvent) { browser.removeEventListener(aEvent.type, onLoad1, true); openConsole(null, function(aHud) { - info("iframe1 height " + aHud.iframe.clientHeight); info("iframe1 root height " + aHud.ui.rootElement.clientHeight); // open tab 2 addTab("data:text/html;charset=utf-8,Web Console test for bug 602572: log bodies checkbox. tab 2"); tabs.push(tab); browser.addEventListener("load", function onLoad2(aEvent) { browser.removeEventListener(aEvent.type, onLoad2, true); openConsole(null, function(aHud) { - info("iframe2 height " + aHud.iframe.clientHeight); info("iframe2 root height " + aHud.ui.rootElement.clientHeight); waitForFocus(startTest, aHud.iframeWindow); }); }, true); }); }, true); } @@ -110,17 +108,16 @@ function onpopupshown2b(aEvent) // Switch to tab 1 and open the Web Console context menu from there. gBrowser.selectedTab = tabs[runCount*2]; waitForFocus(function() { // Find the relevant elements in the Web Console of tab 1. let win1 = tabs[runCount*2].linkedBrowser.contentWindow; let hudId1 = HUDService.getHudIdByWindow(win1); huds[0] = HUDService.hudReferences[hudId1]; - info("iframe1 height " + huds[0].iframe.clientHeight); info("iframe1 root height " + huds[0].ui.rootElement.clientHeight); menuitems[0] = huds[0].ui.rootElement.querySelector("#saveBodies"); menupopups[0] = huds[0].ui.rootElement.querySelector("menupopup"); menupopups[0].addEventListener("popupshown", onpopupshown1, false); menupopups[0].openPopup(); }, tabs[runCount*2].linkedBrowser.contentWindow);
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_611795.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_611795.js @@ -54,24 +54,28 @@ function testConsoleLogRepeats() let outputNode = HUD.outputNode; jsterm.clearOutput(); jsterm.setInputValue("for (let i = 0; i < 10; ++i) console.log('this is a line of reasonably long text that I will use to verify that the repeated text node is of an appropriate size.');"); jsterm.execute(); waitForSuccess({ + timeout: 10000, name: "10 repeated console.log messages", validatorFn: function() { let node = outputNode.querySelector(".webconsole-msg-console"); return node && node.childNodes[3].firstChild.getAttribute("value") == 10; }, successFn: finishTest, - failureFn: finishTest, + failureFn: function() { + info("output content: " + outputNode.textContent); + finishTest(); + }, }); } /** * Unit test for bug 611795: * Repeated CSS messages get collapsed into one. */ function test()
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js @@ -25,86 +25,66 @@ function test() { }); }, true); } function performTest() { let hudId = HUDService.getHudIdByWindow(content); let HUD = HUDService.hudReferences[hudId]; - HUD.jsterm.execute("document"); - let networkMessage = HUD.outputNode.querySelector(".webconsole-msg-network"); ok(networkMessage, "found network message"); let networkLink = networkMessage.querySelector(".webconsole-msg-link"); ok(networkLink, "found network message link"); let popupset = document.getElementById("mainPopupSet"); ok(popupset, "found #mainPopupSet"); let popupsShown = 0; let hiddenPopups = 0; let onpopupshown = function() { + document.removeEventListener("popupshown", onpopupshown, false); popupsShown++; - if (popupsShown == 2) { - document.removeEventListener("popupshown", onpopupshown, false); - executeSoon(function() { - let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]"); - is(popups.length, 2, "found two popups"); + executeSoon(function() { + let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]"); + is(popups.length, 1, "found one popup"); - document.addEventListener("popuphidden", onpopuphidden, false); + document.addEventListener("popuphidden", onpopuphidden, false); - registerCleanupFunction(function() { - is(hiddenPopups, 2, "correct number of popups hidden"); - if (hiddenPopups != 2) { - document.removeEventListener("popuphidden", onpopuphidden, false); - } - }); + registerCleanupFunction(function() { + is(hiddenPopups, 1, "correct number of popups hidden"); + if (hiddenPopups != 1) { + document.removeEventListener("popuphidden", onpopuphidden, false); + } + }); - executeSoon(closeConsole); - }); - } + executeSoon(closeConsole); + }); }; let onpopuphidden = function() { + document.removeEventListener("popuphidden", onpopuphidden, false); hiddenPopups++; - if (hiddenPopups == 2) { - document.removeEventListener("popuphidden", onpopuphidden, false); - executeSoon(function() { - let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]"); - is(popups.length, 0, "no popups found"); + executeSoon(function() { + let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]"); + is(popups.length, 0, "no popups found"); - executeSoon(finishTest); - }); - } + executeSoon(finishTest); + }); }; document.addEventListener("popupshown", onpopupshown, false); registerCleanupFunction(function() { - is(popupsShown, 2, "correct number of popups shown"); - if (popupsShown != 2) { + is(popupsShown, 1, "correct number of popups shown"); + if (popupsShown != 1) { document.removeEventListener("popupshown", onpopupshown, false); } }); - waitForSuccess({ - name: "jsterm output message", - validatorFn: function() - { - return HUD.outputNode.querySelector(".webconsole-msg-output"); - }, - successFn: function() - { - let jstermMessage = HUD.outputNode.querySelector(".webconsole-msg-output"); - EventUtils.sendMouseEvent({ type: "mousedown" }, jstermMessage, HUD.iframeWindow); - EventUtils.sendMouseEvent({ type: "click" }, jstermMessage, HUD.iframeWindow); - EventUtils.sendMouseEvent({ type: "mousedown" }, networkLink, HUD.iframeWindow); - EventUtils.sendMouseEvent({ type: "mouseup" }, networkLink, HUD.iframeWindow); - EventUtils.sendMouseEvent({ type: "click" }, networkLink, HUD.iframeWindow); - }, - failureFn: finishTest, - }); + EventUtils.sendMouseEvent({ type: "mousedown" }, networkLink, HUD.iframeWindow); + EventUtils.sendMouseEvent({ type: "mouseup" }, networkLink, HUD.iframeWindow); + EventUtils.sendMouseEvent({ type: "click" }, networkLink, HUD.iframeWindow); }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632275_getters_document_width.js @@ -1,59 +1,44 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-632275-getters.html"; +let getterValue = null; + function test() { addTab(TEST_URI); browser.addEventListener("load", function onLoad() { browser.removeEventListener("load", onLoad, true); openConsole(null, consoleOpened); }, true); } -function consoleOpened(HUD) { - let jsterm = HUD.jsterm; +function consoleOpened(hud) { + let doc = content.wrappedJSObject.document; + getterValue = doc.foobar._val; + hud.jsterm.execute("console.dir(document)"); + let onOpen = onViewOpened.bind(null, hud); + hud.jsterm.once("variablesview-fetched", onOpen); +} + +function onViewOpened(hud, event, view) +{ let doc = content.wrappedJSObject.document; - let panel = jsterm.openPropertyPanel({ data: { object: doc }}); - - let view = panel.treeView; - let find = function(regex) { - for (let i = 0; i < view.rowCount; i++) { - if (regex.test(view.getCellText(i))) { - return true; - } - } - return false; - }; - - ok(!find(/^(width|height):/), "no document.width/height"); - - panel.destroy(); - - let getterValue = doc.foobar._val; + findVariableViewProperties(view, [ + { name: /^(width|height)$/, dontMatch: 1 }, + { name: "foobar._val", value: getterValue }, + { name: "foobar.val", isGetter: true }, + ], { webconsole: hud }).then(function() { + is(doc.foobar._val, getterValue, "getter did not execute"); + is(doc.foobar.val, getterValue+1, "getter executed"); + is(doc.foobar._val, getterValue+1, "getter executed (recheck)"); - panel = jsterm.openPropertyPanel({ data: { object: doc.foobar }}); - view = panel.treeView; - - is(getterValue, doc.foobar._val, "getter did not execute"); - is(getterValue+1, doc.foobar.val, "getter executed"); - is(getterValue+1, doc.foobar._val, "getter executed (recheck)"); - - ok(find(/^val: Getter$/), - "getter is properly displayed"); - - ok(find(new RegExp("^_val: " + getterValue + "$")), - "getter _val is properly displayed"); - - panel.destroy(); - - executeSoon(function() { - let textContent = HUD.outputNode.textContent; + let textContent = hud.outputNode.textContent; is(textContent.indexOf("document.body.client"), -1, "no document.width/height warning displayed"); finishTest(); }); }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js @@ -21,104 +21,78 @@ function consoleOpened(HUD) { tmp = null; let jsterm = HUD.jsterm; let win = content.wrappedJSObject; // Make sure autocomplete does not walk through iterators and generators. let result = win.gen1.next(); let completion = JSPropertyProvider(win, "gen1."); - is(completion, null, "no matchees for gen1"); + is(completion, null, "no matches for gen1"); ok(!WCU.isObjectInspectable(win.gen1), "gen1 is not inspectable"); is(result+1, win.gen1.next(), "gen1.next() did not execute"); result = win.gen2.next(); completion = JSPropertyProvider(win, "gen2."); - is(completion, null, "no matchees for gen2"); + is(completion, null, "no matches for gen2"); ok(!WCU.isObjectInspectable(win.gen2), "gen2 is not inspectable"); is((result/2+1)*2, win.gen2.next(), "gen2.next() did not execute"); result = win.iter1.next(); is(result[0], "foo", "iter1.next() [0] is correct"); is(result[1], "bar", "iter1.next() [1] is correct"); completion = JSPropertyProvider(win, "iter1."); - is(completion, null, "no matchees for iter1"); + is(completion, null, "no matches for iter1"); ok(!WCU.isObjectInspectable(win.iter1), "iter1 is not inspectable"); result = win.iter1.next(); is(result[0], "baz", "iter1.next() [0] is correct"); is(result[1], "baaz", "iter1.next() [1] is correct"); completion = JSPropertyProvider(content, "iter2."); - is(completion, null, "no matchees for iter2"); + is(completion, null, "no matches for iter2"); ok(!WCU.isObjectInspectable(win.iter2), "iter2 is not inspectable"); completion = JSPropertyProvider(win, "window."); ok(completion, "matches available for window"); ok(completion.matches.length, "matches available for window (length)"); ok(WCU.isObjectInspectable(win), "window is inspectable"); jsterm.clearOutput(); - jsterm.setInputValue("window"); - jsterm.execute(); + jsterm.execute("window"); waitForSuccess({ name: "jsterm window object output", validatorFn: function() { return HUD.outputNode.querySelector(".webconsole-msg-output"); }, successFn: function() { - document.addEventListener("popupshown", function onShown(aEvent) { - document.removeEventListener("popupshown", onShown, false); - executeSoon(testPropertyPanel.bind(null, aEvent.target)); - }, false); - + jsterm.once("variablesview-fetched", testVariablesView.bind(null, HUD)); let node = HUD.outputNode.querySelector(".webconsole-msg-output"); EventUtils.synthesizeMouse(node, 2, 2, {}, HUD.iframeWindow); }, failureFn: finishTest, }); } -function testPropertyPanel(aPanel) { - let tree = aPanel.querySelector("tree"); - let view = tree.view; - let col = tree.columns[0]; - ok(view.rowCount, "Property Panel rowCount"); - - let find = function(display, children) { - for (let i = 0; i < view.rowCount; i++) { - if (view.isContainer(i) == children && - view.getCellText(i, col) == display) { - return true; - } - } - - return false; - }; - - ok(find("gen1: Generator", false), - "gen1 is correctly displayed in the Property Panel"); - - ok(find("gen2: Generator", false), - "gen2 is correctly displayed in the Property Panel"); - - ok(find("iter1: Iterator", false), - "iter1 is correctly displayed in the Property Panel"); - - ok(find("iter2: Object", false), - "iter2 is correctly displayed in the Property Panel"); - - executeSoon(finishTest); +function testVariablesView(aWebconsole, aEvent, aView) { + findVariableViewProperties(aView, [ + { name: "gen1", isGenerator: true }, + { name: "gen2", isGenerator: true }, + { name: "iter1", isIterator: true }, + { name: "iter2", isIterator: true }, + ], { webconsole: aWebconsole }).then(function() { + executeSoon(finishTest); + }); }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js @@ -9,17 +9,17 @@ function test() { addTab("data:text/html;charset=utf-8,Web Console autocompletion bug in document.body"); browser.addEventListener("load", function onLoad() { browser.removeEventListener("load", onLoad, true); openConsole(null, consoleOpened); }, true); } -var gHUD; +let gHUD; function consoleOpened(aHud) { gHUD = aHud; let jsterm = gHUD.jsterm; let popup = jsterm.autocompletePopup; let completeNode = jsterm.completeNode; let tmp = {}; @@ -76,62 +76,33 @@ function autocompletePopupHidden() failureFn: finishTest, }); } function testPropertyPanel() { let jsterm = gHUD.jsterm; jsterm.clearOutput(); - jsterm.setInputValue("document"); - jsterm.execute(); + jsterm.execute("document"); waitForSuccess({ name: "jsterm document object output", validatorFn: function() { return gHUD.outputNode.querySelector(".webconsole-msg-output"); }, successFn: function() { - document.addEventListener("popupshown", function onShown(aEvent) { - document.removeEventListener("popupshown", onShown, false); - executeSoon(propertyPanelShown.bind(null, aEvent.target)); - }, false); - + jsterm.once("variablesview-fetched", onVariablesViewReady); let node = gHUD.outputNode.querySelector(".webconsole-msg-output"); EventUtils.synthesizeMouse(node, 2, 2, {}, gHUD.iframeWindow); }, failureFn: finishTest, }); } -function propertyPanelShown(aPanel) +function onVariablesViewReady(aEvent, aView) { - let tree = aPanel.querySelector("tree"); - let view = tree.view; - let col = tree.columns[0]; - ok(view.rowCount, "Property Panel rowCount"); - - let foundBody = false; - let propPanelProps = []; - for (let idx = 0; idx < view.rowCount; ++idx) { - let text = view.getCellText(idx, col); - if (text == "body: HTMLBodyElement" || text == "body: Object") - foundBody = true; - propPanelProps.push(text.split(":")[0]); - } - - // NB: We pull the properties off the prototype, rather than off object itself, - // so that expandos like |constructor|, which the propPanel can't see, are not - // included. - for (let prop in Object.getPrototypeOf(content.document).wrappedObject) { - if (prop == "inputEncoding") { - continue; - } - ok(propPanelProps.indexOf(prop) != -1, "Property |" + prop + "| should be reflected in propertyPanel"); - } - - ok(foundBody, "found document.body"); - - executeSoon(finishTest); + findVariableViewProperties(aView, [ + { name: "__proto__.body", value: "[object HTMLBodyElement]" }, + ], { webconsole: gHUD }).then(finishTest); }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js @@ -11,46 +11,19 @@ function test() { "object with a dir method"); browser.addEventListener("load", function onLoad(aEvent) { browser.removeEventListener(aEvent.type, onLoad, true); openConsole(null, consoleOpened); }, true); } function consoleOpened(hud) { - outputNode = hud.outputNode; - content.console.dir(content.document); - waitForSuccess({ - name: "console.dir displayed", - validatorFn: function() - { - return outputNode.textContent.indexOf("[object HTMLDocument") > -1; - }, - successFn: testConsoleDir.bind(null, outputNode), - failureFn: finishTest, - }); + hud.jsterm.execute("console.dir(document)"); + hud.jsterm.once("variablesview-fetched", testConsoleDir.bind(null, hud)); } -function testConsoleDir(outputNode) { - let msg = outputNode.querySelectorAll(".webconsole-msg-inspector"); - is(msg.length, 1, "one message node displayed"); - let view = msg[0].propertyTreeView; - let foundQSA = false; - let foundLocation = false; - let foundWrite = false; - for (let i = 0; i < view.rowCount; i++) { - let text = view.getCellText(i); - if (text == "querySelectorAll: function querySelectorAll()") { - foundQSA = true; - } - else if (text == "location: Location") { - foundLocation = true; - } - else if (text == "write: function write()") { - foundWrite = true; - } - } - ok(foundQSA, "found document.querySelectorAll"); - ok(foundLocation, "found document.location"); - ok(foundWrite, "found document.write"); - msg = view = outputNode = null; - executeSoon(finishTest); +function testConsoleDir(hud, ev, view) { + findVariableViewProperties(view, [ + { name: "__proto__.querySelectorAll", value: "[object Function]" }, + { name: "location", value: "[object Location]" }, + { name: "__proto__.write", value: "[object Function]" }, + ], { webconsole: hud }).then(finishTest); }
--- a/browser/devtools/webconsole/test/browser_webconsole_chrome.js +++ b/browser/devtools/webconsole/test/browser_webconsole_chrome.js @@ -11,17 +11,17 @@ function test() { browser.removeEventListener("load", onLoad, true); openConsole(null, testChrome); }, true); } function testChrome(hud) { ok(hud, "we have a console"); - ok(hud.iframe, "we have the console iframe"); + ok(hud.iframeWindow, "we have the console UI window"); let jsterm = hud.jsterm; ok(jsterm, "we have a jsterm"); let input = jsterm.inputNode; ok(hud.outputNode, "we have an output node"); // Test typing 'docu'.
--- a/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js +++ b/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js @@ -123,17 +123,17 @@ function testConsoleLoggingAPI(aMethod) // test for multiple arguments. console[aMethod]("foo", "bar"); waitForSuccess({ name: "show both console arguments for " + aMethod, validatorFn: function() { let node = outputNode.querySelector(".hud-msg-node"); - return node && /foo bar/.test(node.textContent); + return node && /"foo" "bar"/.test(node.textContent); }, successFn: nextTest, failureFn: nextTest, }); yield; testDriver.next(); yield;
--- a/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js +++ b/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js @@ -14,30 +14,30 @@ function test() { openConsole(null, testExecutionScope); }, true); } function testExecutionScope(hud) { let jsterm = hud.jsterm; jsterm.clearOutput(); - jsterm.execute("window.location;"); + jsterm.execute("window.location.href;"); waitForSuccess({ name: "jsterm execution output (two nodes)", validatorFn: function() { return jsterm.outputNode.querySelectorAll(".hud-msg-node").length == 2; }, successFn: function() { let nodes = jsterm.outputNode.querySelectorAll(".hud-msg-node"); - is(/window.location;/.test(nodes[0].textContent), true, - "'window.location;' written to output"); + is(/window.location.href;/.test(nodes[0].textContent), true, + "'window.location.href;' written to output"); isnot(nodes[1].textContent.indexOf(TEST_URI), -1, "command was executed in the window scope"); executeSoon(finishTest); }, failureFn: finishTest, });
--- a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js +++ b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js @@ -130,17 +130,17 @@ function testJSTerm(hud) nextTest(); }, failureFn: nextTest, }); yield; jsterm.clearOutput(); jsterm.execute("pprint({b:2, a:1})"); - checkResult("a: 1\n b: 2", "pprint()", 1); + checkResult('" b: 2\n a: 1"', "pprint()", 1); yield; // check instanceof correctness, bug 599940 jsterm.clearOutput(); jsterm.execute("[] instanceof Array"); checkResult("true", "[] instanceof Array == true", 1); yield; @@ -166,17 +166,17 @@ function testJSTerm(hud) jsterm.clearOutput(); jsterm.execute("keys(window)"); checkResult(null, "keys(window)", 1); yield; // bug 614561 jsterm.clearOutput(); jsterm.execute("pprint('hi')"); - checkResult('0: "h"\n 1: "i"', "pprint('hi')", 1); + checkResult('" 0: "h"\n 1: "i""', "pprint('hi')", 1); yield; // check that pprint(function) shows function source, bug 618344 jsterm.clearOutput(); jsterm.execute("pprint(print)"); checkResult(function(nodes) { return nodes[0].textContent.indexOf("aOwner.helperResult") > -1; }, "pprint(function) shows source", 1);
--- a/browser/devtools/webconsole/test/browser_webconsole_output_order.js +++ b/browser/devtools/webconsole/test/browser_webconsole_output_order.js @@ -29,16 +29,16 @@ function testOutputOrder(hud) { { return outputNode.querySelectorAll(".hud-msg-node").length == 3; }, successFn: function() { let nodes = outputNode.querySelectorAll(".hud-msg-node"); let executedStringFirst = /console\.log\('foo', 'bar'\);/.test(nodes[0].textContent); - let outputSecond = /foo bar/.test(nodes[2].textContent); + let outputSecond = /"foo" "bar"/.test(nodes[2].textContent); ok(executedStringFirst && outputSecond, "executed string comes first"); finishTest(); }, failureFn: finishTest, }); }
deleted file mode 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_property_panel.js +++ /dev/null @@ -1,68 +0,0 @@ -/* vim:set ts=2 sw=2 sts=2 et: */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// Tests the functionality of the "property panel", which allows JavaScript -// objects and DOM nodes to be inspected. - -const TEST_URI = "data:text/html;charset=utf8,<p>property panel test"; - -function test() { - addTab(TEST_URI); - browser.addEventListener("load", function onLoad() { - browser.removeEventListener("load", onLoad, true); - openConsole(null, testPropertyPanel); - }, true); -} - -function testPropertyPanel(hud) { - let jsterm = hud.jsterm; - - let propPanel = jsterm.openPropertyPanel({ - data: { - object: [ - 1, - /abc/, - null, - undefined, - function test() {}, - {} - ] - } - }); - is (propPanel.treeView.rowCount, 6, "six elements shown in propertyPanel"); - propPanel.destroy(); - - propPanel = jsterm.openPropertyPanel({ - data: { - object: { - "0.02": 0, - "0.01": 1, - "02": 2, - "1": 3, - "11": 4, - "1.2": 5, - "1.1": 6, - "foo": 7, - "bar": 8 - } - } - }); - is (propPanel.treeView.rowCount, 9, "nine elements shown in propertyPanel"); - - let view = propPanel.treeView; - is (view.getCellText(0), "0.01: 1", "1. element is okay"); - is (view.getCellText(1), "0.02: 0", "2. element is okay"); - is (view.getCellText(2), "1: 3", "3. element is okay"); - is (view.getCellText(3), "1.1: 6", "4. element is okay"); - is (view.getCellText(4), "1.2: 5", "5. element is okay"); - is (view.getCellText(5), "02: 2", "6. element is okay"); - is (view.getCellText(6), "11: 4", "7. element is okay"); - is (view.getCellText(7), "bar: 8", "8. element is okay"); - is (view.getCellText(8), "foo: 7", "9. element is okay"); - propPanel.destroy(); - - executeSoon(finishTest); -} -
--- a/browser/devtools/webconsole/test/head.js +++ b/browser/devtools/webconsole/test/head.js @@ -9,16 +9,17 @@ let HUDService = tempScope.HUDService; Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tempScope); let WebConsoleUtils = tempScope.WebConsoleUtils; Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope); let gDevTools = tempScope.gDevTools; Cu.import("resource:///modules/devtools/Target.jsm", tempScope); let TargetFactory = tempScope.TargetFactory; Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope); let console = tempScope.console; +let Promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise; const WEBCONSOLE_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; let WCU_l10n = new WebConsoleUtils.l10n(WEBCONSOLE_STRINGS_URI); function log(aMsg) { dump("*** WebConsoleTest: " + aMsg + "\n"); } @@ -138,17 +139,19 @@ function findLogEntry(aString) * @param function [aCallback] * Optional function to invoke after the Web Console completes * initialization (web-console-created). */ function openConsole(aTab, aCallback = function() { }) { let target = TargetFactory.forTab(aTab || tab); gDevTools.showToolbox(target, "webconsole").then(function(toolbox) { - aCallback(toolbox.getCurrentPanel().hud); + let hud = toolbox.getCurrentPanel().hud; + hud.jsterm._lazyVariablesView = false; + aCallback(hud); }); } /** * Close the Web Console for the given tab. * * @param nsIDOMElement [aTab] * Optional tab element for which you want close the Web Console. The @@ -160,19 +163,17 @@ function openConsole(aTab, aCallback = f function closeConsole(aTab, aCallback = function() { }) { let target = TargetFactory.forTab(aTab || tab); let toolbox = gDevTools.getToolbox(target); if (toolbox) { let panel = toolbox.getPanel("webconsole"); if (panel) { let hudId = panel.hud.hudId; - toolbox.destroy().then(function() { - executeSoon(aCallback.bind(null, hudId)); - }).then(null, console.error); + toolbox.destroy().then(aCallback.bind(null, hudId)).then(null, console.debug); } else { toolbox.destroy().then(aCallback.bind(null)); } } else { aCallback(); } @@ -228,32 +229,41 @@ function waitForOpenContextMenu(aContext EventUtils.synthesizeMouse(targetElement, 2, 2, eventDetails, targetElement.ownerDocument.defaultView); } function finishTest() { browser = hudId = hud = filterBox = outputNode = cs = null; + if (HUDConsoleUI.browserConsole) { + HUDConsoleUI.toggleBrowserConsole().then(finishTest); + return; + } + let hud = HUDService.getHudByWindow(content); if (!hud) { finish(); return; } if (hud.jsterm) { hud.jsterm.clearOutput(true); } closeConsole(hud.target.tab, finish); hud = null; } function tearDown() { + if (HUDConsoleUI.browserConsole) { + HUDConsoleUI.toggleBrowserConsole(); + } + let target = TargetFactory.forTab(gBrowser.selectedTab); gDevTools.closeToolbox(target); while (gBrowser.tabs.length > 1) { gBrowser.removeCurrentTab(); } WCU_l10n = tab = browser = hudId = hud = filterBox = outputNode = cs = null; } @@ -310,8 +320,450 @@ function waitForSuccess(aOptions) function openInspector(aCallback, aTab = gBrowser.selectedTab) { let target = TargetFactory.forTab(aTab); gDevTools.showToolbox(target, "inspector").then(function(toolbox) { aCallback(toolbox.getCurrentPanel()); }); } + +/** + * Find variables or properties in a VariablesView instance. + * + * @param object aView + * The VariablesView instance. + * @param array aRules + * The array of rules you want to match. Each rule is an object with: + * - name (string|regexp): property name to match. + * - value (string|regexp): property value to match. + * - isIterator (boolean): check if the property is an iterator. + * - isGetter (boolean): check if the property is a getter. + * - isGenerator (boolean): check if the property is a generator. + * - dontMatch (boolean): make sure the rule doesn't match any property. + * @param object aOptions + * Options for matching: + * - webconsole: the WebConsole instance we work with. + * @return object + * A Promise object that is resolved when all the rules complete + * matching. The resolved callback is given an array of all the rules + * you wanted to check. Each rule has a new property: |matchedProp| + * which holds a reference to the Property object instance from the + * VariablesView. If the rule did not match, then |matchedProp| is + * undefined. + */ +function findVariableViewProperties(aView, aRules, aOptions) +{ + // Initialize the search. + function init() + { + // Separate out the rules that require expanding properties throughout the + // view. + let expandRules = []; + let rules = aRules.filter((aRule) => { + if (typeof aRule.name == "string" && aRule.name.indexOf(".") > -1) { + expandRules.push(aRule); + return false; + } + return true; + }); + + // Search through the view those rules that do not require any properties to + // be expanded. Build the array of matchers, outstanding promises to be + // resolved. + let outstanding = []; + finder(rules, aView, outstanding); + + // Process the rules that need to expand properties. + let lastStep = processExpandRules.bind(null, expandRules); + + // Return the results - a Promise resolved to hold the updated aRules array. + let returnResults = onAllRulesMatched.bind(null, aRules); + + return Promise.all(outstanding).then(lastStep).then(returnResults); + } + + function onMatch(aProp, aRule, aMatched) + { + if (aMatched && !aRule.matchedProp) { + aRule.matchedProp = aProp; + } + } + + function finder(aRules, aVar, aPromises) + { + for (let [id, prop] in aVar) { + for (let rule of aRules) { + let matcher = matchVariablesViewProperty(prop, rule, aOptions); + aPromises.push(matcher.then(onMatch.bind(null, prop, rule))); + } + } + } + + function processExpandRules(aRules) + { + let rule = aRules.shift(); + if (!rule) { + return Promise.resolve(null); + } + + let deferred = Promise.defer(); + let expandOptions = { + rootVariable: aView, + expandTo: rule.name, + webconsole: aOptions.webconsole, + }; + + variablesViewExpandTo(expandOptions).then(function onSuccess(aProp) { + let name = rule.name; + let lastName = name.split(".").pop(); + rule.name = lastName; + + let matched = matchVariablesViewProperty(aProp, rule, aOptions); + return matched.then(onMatch.bind(null, aProp, rule)).then(function() { + rule.name = name; + }); + }, function onFailure() { + return Promise.resolve(null); + }).then(processExpandRules.bind(null, aRules)).then(function() { + deferred.resolve(null); + }); + + return deferred.promise; + } + + function onAllRulesMatched(aRules) + { + for (let rule of aRules) { + let matched = rule.matchedProp; + if (matched && !rule.dontMatch) { + ok(true, "rule " + rule.name + " matched for property " + matched.name); + } + else if (matched && rule.dontMatch) { + ok(false, "rule " + rule.name + " should not match property " + + matched.name); + } + else { + ok(rule.dontMatch, "rule " + rule.name + " did not match any property"); + } + } + return aRules; + } + + return init(); +} + +/** + * Check if a given Property object from the variables view matches the given + * rule. + * + * @param object aProp + * The variable's view Property instance. + * @param object aRule + * Rules for matching the property. See findVariableViewProperties() for + * details. + * @param object aOptions + * Options for matching. See findVariableViewProperties(). + * @return object + * A Promise that is resolved when all the checks complete. Resolution + * result is a boolean that tells your promise callback the match + * result: true or false. + */ +function matchVariablesViewProperty(aProp, aRule, aOptions) +{ + function resolve(aResult) { + return Promise.resolve(aResult); + } + + if (aRule.name) { + let match = aRule.name instanceof RegExp ? + aRule.name.test(aProp.name) : + aProp.name == aRule.name; + if (!match) { + return resolve(false); + } + } + + if (aRule.value) { + let displayValue = aProp.displayValue; + if (aProp.displayValueClassName == "token-string") { + displayValue = displayValue.substring(1, displayValue.length - 1); + } + + let match = aRule.value instanceof RegExp ? + aRule.value.test(displayValue) : + displayValue == aRule.value; + if (!match) { + info("rule " + aRule.name + " did not match value, expected '" + + aRule.value + "', found '" + displayValue + "'"); + return resolve(false); + } + } + + if ("isGetter" in aRule) { + let isGetter = !!(aProp.getter && aProp.get("get")); + if (aRule.isGetter != isGetter) { + info("rule " + aRule.name + " getter test failed"); + return resolve(false); + } + } + + if ("isGenerator" in aRule) { + let isGenerator = aProp.displayValue == "[object Generator]"; + if (aRule.isGenerator != isGenerator) { + info("rule " + aRule.name + " generator test failed"); + return resolve(false); + } + } + + let outstanding = []; + + if ("isIterator" in aRule) { + let isIterator = isVariableViewPropertyIterator(aProp, aOptions.webconsole); + outstanding.push(isIterator.then((aResult) => { + if (aResult != aRule.isIterator) { + info("rule " + aRule.name + " iterator test failed"); + } + return aResult == aRule.isIterator; + })); + } + + outstanding.push(Promise.resolve(true)); + + return Promise.all(outstanding).then(function _onMatchDone(aResults) { + let ruleMatched = aResults.indexOf(false) == -1; + return resolve(ruleMatched); + }); +} + +/** + * Check if the given variables view property is an iterator. + * + * @param object aProp + * The Property instance you want to check. + * @param object aWebConsole + * The WebConsole instance to work with. + * @return object + * A Promise that is resolved when the check completes. The resolved + * callback is given a boolean: true if the property is an iterator, or + * false otherwise. + */ +function isVariableViewPropertyIterator(aProp, aWebConsole) +{ + if (aProp.displayValue == "[object Iterator]") { + return Promise.resolve(true); + } + + let deferred = Promise.defer(); + + variablesViewExpandTo({ + rootVariable: aProp, + expandTo: "__proto__.__iterator__", + webconsole: aWebConsole, + }).then(function onSuccess(aProp) { + deferred.resolve(true); + }, function onFailure() { + deferred.resolve(false); + }); + + return deferred.promise; +} + + +/** + * Recursively expand the variables view up to a given property. + * + * @param aOptions + * Options for view expansion: + * - rootVariable: start from the given scope/variable/property. + * - expandTo: string made up of property names you want to expand. + * For example: "body.firstChild.nextSibling" given |rootVariable: + * document|. + * - webconsole: a WebConsole instance. If this is not provided all + * property expand() calls will be considered sync. Things may fail! + * @return object + * A Promise that is resolved only when the last property in |expandTo| + * is found, and rejected otherwise. Resolution reason is always the + * last property - |nextSibling| in the example above. Rejection is + * always the last property that was found. + */ +function variablesViewExpandTo(aOptions) +{ + let root = aOptions.rootVariable; + let expandTo = aOptions.expandTo.split("."); + let jsterm = (aOptions.webconsole || {}).jsterm; + let lastDeferred = Promise.defer(); + + function fetch(aProp) + { + if (!aProp.onexpand) { + ok(false, "property " + aProp.name + " cannot be expanded: !onexpand"); + return Promise.reject(aProp); + } + + let deferred = Promise.defer(); + + if (aProp._fetched || !jsterm) { + executeSoon(function() { + deferred.resolve(aProp); + }); + } + else { + jsterm.once("variablesview-fetched", function _onFetchProp() { + executeSoon(() => deferred.resolve(aProp)); + }); + } + + aProp.expand(); + + return deferred.promise; + } + + function getNext(aProp) + { + let name = expandTo.shift(); + let newProp = aProp.get(name); + + if (expandTo.length > 0) { + ok(newProp, "found property " + name); + if (newProp) { + fetch(newProp).then(getNext, fetchError); + } + else { + lastDeferred.reject(aProp); + } + } + else { + if (newProp) { + lastDeferred.resolve(newProp); + } + else { + lastDeferred.reject(aProp); + } + } + } + + function fetchError(aProp) + { + lastDeferred.reject(aProp); + } + + if (!root._fetched) { + fetch(root).then(getNext, fetchError); + } + else { + getNext(root); + } + + return lastDeferred.promise; +} + + +/** + * Update the content of a property in the variables view. + * + * @param object aOptions + * Options for the property update: + * - property: the property you want to change. + * - field: string that tells what you want to change: + * - use "name" to change the property name, + * - or "value" to change the property value. + * - string: the new string to write into the field. + * - webconsole: reference to the Web Console instance we work with. + * - callback: function to invoke after the property is updated. + */ +function updateVariablesViewProperty(aOptions) +{ + let view = aOptions.property._variablesView; + view.window.focus(); + aOptions.property.focus(); + + switch (aOptions.field) { + case "name": + EventUtils.synthesizeKey("VK_ENTER", { shiftKey: true }, view.window); + break; + case "value": + EventUtils.synthesizeKey("VK_ENTER", {}, view.window); + break; + default: + throw new Error("options.field is incorrect"); + return; + } + + executeSoon(() => { + EventUtils.synthesizeKey("A", { accelKey: true }, view.window); + + for (let c of aOptions.string) { + EventUtils.synthesizeKey(c, {}, gVariablesView.window); + } + + if (aOptions.webconsole) { + aOptions.webconsole.jsterm.once("variablesview-fetched", aOptions.callback); + } + + EventUtils.synthesizeKey("VK_ENTER", {}, view.window); + + if (!aOptions.webconsole) { + executeSoon(aOptions.callback); + } + }); +} + +/** + * Open the JavaScript debugger. + * + * @param object aOptions + * Options for opening the debugger: + * - tab: the tab you want to open the debugger for. + * @return object + * A Promise that is resolved once the debugger opens, or rejected if + * the open fails. The resolution callback is given one argument, an + * object that holds the following properties: + * - target: the Target object for the Tab. + * - toolbox: the Toolbox instance. + * - panel: the jsdebugger panel instance. + * - panelWin: the window object of the panel iframe. + */ +function openDebugger(aOptions = {}) +{ + if (!aOptions.tab) { + aOptions.tab = gBrowser.selectedTab; + } + + let deferred = Promise.defer(); + + let target = TargetFactory.forTab(aOptions.tab); + let toolbox = gDevTools.getToolbox(target); + let dbgPanelAlreadyOpen = toolbox.getPanel("jsdebugger"); + + gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(aToolbox) { + let panel = aToolbox.getCurrentPanel(); + let panelWin = panel.panelWin; + + panel._view.Variables.lazyEmpty = false; + panel._view.Variables.lazyAppend = false; + + let resolveObject = { + target: target, + toolbox: aToolbox, + panel: panel, + panelWin: panelWin, + }; + + if (dbgPanelAlreadyOpen) { + deferred.resolve(resolveObject); + } + else { + panelWin.addEventListener("Debugger:AfterSourcesAdded", + function onAfterSourcesAdded() { + panelWin.removeEventListener("Debugger:AfterSourcesAdded", + onAfterSourcesAdded); + deferred.resolve(resolveObject); + }); + } + }, function onFailure(aReason) { + console.debug("failed to open the toolbox for 'jsdebugger'", aReason); + deferred.reject(aReason); + }); + + return deferred.promise; +} +
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/test-eval-in-stackframe.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en"> + <head> + <meta charset="utf8"> + <!-- + - Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ + --> + <title>Test for bug 783499 - use the debugger API in the web console</title> + <script> + var foo = "globalFooBug783499"; + var fooObj = { + testProp: "testValue", + }; + + function firstCall() + { + var foo = "fooFirstCall"; + var foo3 = "foo3FirstCall"; + secondCall(); + } + + function secondCall() + { + var foo2 = "foo2SecondCall"; + var fooObj = { + testProp2: "testValue2", + }; + var fooObj2 = { + testProp22: "testValue22", + }; + debugger; + } + </script> + </head> + <body> + <p>Hello world!</p> + </body> +</html>
--- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -14,45 +14,57 @@ Cu.import("resource://gre/modules/XPCOMU XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); -XPCOMUtils.defineLazyModuleGetter(this, "PropertyPanel", - "resource:///modules/PropertyPanel.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "PropertyTreeView", - "resource:///modules/PropertyPanel.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "GripClient", + "resource://gre/modules/devtools/dbg-client.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetworkPanel", "resource:///modules/NetworkPanel.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup", "resource:///modules/devtools/AutocompletePopup.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", "resource://gre/modules/devtools/WebConsoleUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/commonjs/sdk/core/promise.js"); +XPCOMUtils.defineLazyModuleGetter(this, "VariablesView", + "resource:///modules/devtools/VariablesView.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ToolSidebar", + "resource:///modules/devtools/Sidebar.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", + "resource:///modules/devtools/EventEmitter.jsm"); + const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; let l10n = new WebConsoleUtils.l10n(STRINGS_URI); // The XUL namespace. const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/en/Security/MixedContent"; const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; +const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul"; + +const CONSOLE_DIR_VIEW_HEIGHT = 0.6; + +const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"]; + // The amount of time in milliseconds that must pass between messages to // trigger the display of a new group. const NEW_GROUP_DELAY = 5000; // The amount of time in milliseconds that we wait before performing a live // search. const SEARCH_DELAY = 200; @@ -209,16 +221,23 @@ WebConsoleFrame.prototype = { /** * Getter for the xul:popupset that holds any popups we open. * @type nsIDOMElement */ get popupset() this.owner.mainPopupSet, /** + * Holds the initialization Promise object. + * @private + * @type object + */ + _initDefer: null, + + /** * Holds the network requests currently displayed by the Web Console. Each key * represents the connection ID and the value is network request information. * @private * @type object */ _networkRequests: null, /** @@ -360,40 +379,37 @@ WebConsoleFrame.prototype = { * * @private * @return object * A Promise object that is resolved/reject based on the connection * result. */ _initConnection: function WCF__initConnection() { - let deferred = Promise.defer(); - + if (this._initDefer) { + return this._initDefer.promise; + } + + this._initDefer = Promise.defer(); this.proxy = new WebConsoleConnectionProxy(this, this.owner.target); - let onSuccess = function() { + this.proxy.connect().then(() => { // on success this.saveRequestAndResponseBodies = this._saveRequestAndResponseBodies; - deferred.resolve(this); - }.bind(this); - - let onFailure = function(aReason) { + this._initDefer.resolve(this); + }, (aReason) => { // on failure let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR, aReason.error + ": " + aReason.message); this.outputMessage(CATEGORY_JS, node); - deferred.reject(aReason); - }.bind(this); - - let sendNotification = function() { + this._initDefer.reject(aReason); + }).then(() => { let id = WebConsoleUtils.supportsString(this.hudId); Services.obs.notifyObservers(id, "web-console-created", null); - }.bind(this); - - this.proxy.connect().then(onSuccess, onFailure).then(sendNotification); - - return deferred.promise; + }); + + return this._initDefer.promise; }, /** * Find the Web Console UI elements and setup event listeners as needed. * @private */ _initUI: function WCF__initUI() { @@ -949,55 +965,42 @@ WebConsoleFrame.prototype = { logConsoleAPIMessage: function WCF_logConsoleAPIMessage(aMessage) { let body = null; let clipboardText = null; let sourceURL = aMessage.filename; let sourceLine = aMessage.lineNumber; let level = aMessage.level; let args = aMessage.arguments; - let objectActors = []; + let objectActors = new Set(); // Gather the actor IDs. - args.forEach(function(aValue) { - if (aValue && typeof aValue == "object" && aValue.actor) { - objectActors.push(aValue.actor); - let displayStringIsLong = typeof aValue.displayString == "object" && - aValue.displayString.type == "longString"; - if (displayStringIsLong) { - objectActors.push(aValue.displayString.actor); - } + args.forEach((aValue) => { + if (WebConsoleUtils.isActorGrip(aValue)) { + objectActors.add(aValue.actor); } - }, this); + }); switch (level) { case "log": case "info": case "warn": case "error": case "debug": - case "dir": - case "groupEnd": { + case "dir": { body = { arguments: args }; let clipboardArray = []; - args.forEach(function(aValue) { - clipboardArray.push(WebConsoleUtils.objectActorGripToString(aValue)); - if (aValue && typeof aValue == "object" && aValue.actor) { - let displayStringIsLong = typeof aValue.displayString == "object" && - aValue.displayString.type == "longString"; - if (aValue.type == "longString" || displayStringIsLong) { - clipboardArray.push(l10n.getStr("longStringEllipsis")); - } + args.forEach((aValue) => { + clipboardArray.push(VariablesView.getString(aValue)); + if (aValue && typeof aValue == "object" && + aValue.type == "longString") { + clipboardArray.push(l10n.getStr("longStringEllipsis")); } - }, this); + }); clipboardText = clipboardArray.join(" "); - - if (level == "dir") { - body.objectProperties = aMessage.objectProperties; - } break; } case "trace": { let filename = WebConsoleUtils.abbreviateSourceURL(aMessage.filename); let functionName = aMessage.functionName || l10n.getStr("stacktrace.anonymousFunction"); @@ -1017,16 +1020,22 @@ WebConsoleFrame.prototype = { } case "group": case "groupCollapsed": clipboardText = body = aMessage.groupName; this.groupDepth++; break; + case "groupEnd": + if (this.groupDepth > 0) { + this.groupDepth--; + } + break; + case "time": { let timer = aMessage.timer; if (!timer) { return; } if (timer.error) { Cu.reportError(l10n.getStr(timer.error)); return; @@ -1055,62 +1064,41 @@ WebConsoleFrame.prototype = { // we ignore their arguments. switch (level) { case "group": case "groupCollapsed": case "groupEnd": case "trace": case "time": case "timeEnd": - objectActors.forEach(this._releaseObject, this); - objectActors = []; + for (let actor of objectActors) { + this._releaseObject(actor); + } + objectActors.clear(); } if (level == "groupEnd") { - if (this.groupDepth > 0) { - this.groupDepth--; - } return; // no need to continue } let node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body, sourceURL, sourceLine, clipboardText, level, aMessage.timeStamp); - if (objectActors.length) { + if (objectActors.size > 0) { node._objectActors = objectActors; } - // Make the node bring up the property panel, to allow the user to inspect + // Make the node bring up the variables view, to allow the user to inspect // the stack trace. if (level == "trace") { node._stacktrace = aMessage.stacktrace; - this.makeOutputMessageLink(node, function _traceNodeClickCallback() { - if (node._panelOpen) { - return; - } - - let options = { - anchor: node, - data: { object: node._stacktrace }, - }; - - let propPanel = this.jsterm.openPropertyPanel(options); - propPanel.panel.setAttribute("hudId", this.hudId); - }.bind(this)); - } - - if (level == "dir") { - // Initialize the inspector message node, by setting the PropertyTreeView - // object on the tree view. This has to be done *after* the node is - // shown, because the tree binding must be attached first. - node._onOutput = function _onMessageOutput() { - node.querySelector("tree").view = node.propertyTreeView; - }; + this.makeOutputMessageLink(node, () => + this.jsterm.openVariablesView({ rawObject: node._stacktrace })); } return node; }, /** * Handle ConsoleAPICall objects received from the server. This method outputs * the window.console API call. @@ -1123,59 +1111,29 @@ WebConsoleFrame.prototype = { this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage]); }, /** * The click event handler for objects shown inline coming from the * window.console API. * * @private - * @param nsIDOMNode aMessage - * The message element this handler corresponds to. * @param nsIDOMNode aAnchor * The object inspector anchor element. This is the clickable element * in the console.log message we display. * @param object aObjectActor * The object actor grip. */ - _consoleLogClick: - function WCF__consoleLogClick(aMessage, aAnchor, aObjectActor) + _consoleLogClick: function WCF__consoleLogClick(aAnchor, aObjectActor) { - if (aAnchor._panelOpen) { - return; - } - let options = { - title: aAnchor.textContent, - anchor: aAnchor, - - // Data to inspect. - data: { - objectPropertiesProvider: this.objectPropertiesProvider.bind(this), - releaseObject: this._releaseObject.bind(this), - }, + label: aAnchor.textContent, + objectActor: aObjectActor, }; - - let propPanel; - let onPopupHide = function _onPopupHide() { - propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); - - if (!aMessage.parentNode && aMessage._objectActors) { - aMessage._objectActors.forEach(this._releaseObject, this); - aMessage._objectActors = null; - } - }.bind(this); - - this.objectPropertiesProvider(aObjectActor.actor, - function _onObjectProperties(aProperties) { - options.data.objectProperties = aProperties; - propPanel = this.jsterm.openPropertyPanel(options); - propPanel.panel.setAttribute("hudId", this.hudId); - propPanel.panel.addEventListener("popuphiding", onPopupHide, false); - }.bind(this)); + this.jsterm.openVariablesView(options); }, /** * Reports an error in the page source, either JavaScript or CSS. * * @param nsIScriptError aScriptError * The error message to report. * @return nsIDOMElement|undefined @@ -1903,19 +1861,21 @@ WebConsoleFrame.prototype = { * * @private * @param array aItem * The item you want to remove from the output queue. */ _pruneItemFromQueue: function WCF__pruneItemFromQueue(aItem) { let [category, methodOrNode, args] = aItem; - if (typeof methodOrNode != "function" && - methodOrNode._objectActors && !methodOrNode._panelOpen) { - methodOrNode._objectActors.forEach(this._releaseObject, this); + if (typeof methodOrNode != "function" && methodOrNode._objectActors) { + for (let actor of methodOrNode._objectActors) { + this._releaseObject(actor); + } + methodOrNode._objectActors.clear(); } if (category == CATEGORY_NETWORK) { let connectionId = null; if (methodOrNode == this.logNetEvent) { connectionId = args[0]; } else if (typeof methodOrNode != "function") { @@ -1923,40 +1883,21 @@ WebConsoleFrame.prototype = { } if (connectionId && connectionId in this._networkRequests) { delete this._networkRequests[connectionId]; this._releaseObject(connectionId); } } else if (category == CATEGORY_WEBDEV && methodOrNode == this.logConsoleAPIMessage) { - let level = args[0].level; - let releaseObject = function _releaseObject(aValue) { - if (aValue && typeof aValue == "object" && aValue.actor) { + args[0].arguments.forEach((aValue) => { + if (WebConsoleUtils.isActorGrip(aValue)) { this._releaseObject(aValue.actor); } - }.bind(this); - switch (level) { - case "log": - case "info": - case "warn": - case "error": - case "debug": - case "dir": - case "groupEnd": { - args[0].arguments.forEach(releaseObject); - if (level == "dir") { - args[0].objectProperties.forEach(function(aObject) { - ["value", "get", "set"].forEach(function(aProp) { - releaseObject(aObject[aProp]); - }); - }); - } - } - } + }); } }, /** * Ensures that the number of message nodes of type aCategory don't exceed that * category's line limit by removing old messages as needed. * * @param integer aCategory @@ -1974,62 +1915,51 @@ WebConsoleFrame.prototype = { let n = Math.max(0, messageNodes.length - logLimit); let toRemove = Array.prototype.slice.call(messageNodes, 0, n); toRemove.forEach(this.removeOutputMessage, this); return n; }, /** - * Destroy the property inspector message node. This performs the necessary - * cleanup for the tree widget and removes it from the DOM. - * - * @param nsIDOMNode aMessageNode - * The message node that contains the property inspector from a - * console.dir call. - */ - pruneConsoleDirNode: function WCF_pruneConsoleDirNode(aMessageNode) - { - if (aMessageNode.parentNode) { - aMessageNode.parentNode.removeChild(aMessageNode); - } - - let tree = aMessageNode.querySelector("tree"); - tree.parentNode.removeChild(tree); - aMessageNode.propertyTreeView.data = null; - aMessageNode.propertyTreeView = null; - tree.view = null; - }, - - /** * Remove a given message from the output. * * @param nsIDOMNode aNode * The message node you want to remove. */ removeOutputMessage: function WCF_removeOutputMessage(aNode) { - if (aNode._objectActors && !aNode._panelOpen) { - aNode._objectActors.forEach(this._releaseObject, this); + if (aNode._objectActors) { + for (let actor of aNode._objectActors) { + this._releaseObject(actor); + } + aNode._objectActors.clear(); } if (aNode.classList.contains("webconsole-msg-cssparser")) { let repeatNode = aNode.getElementsByClassName("webconsole-msg-repeat")[0]; if (repeatNode && repeatNode._uid) { delete this._cssNodes[repeatNode._uid]; } } else if (aNode._connectionId && aNode.classList.contains("webconsole-msg-network")) { delete this._networkRequests[aNode._connectionId]; this._releaseObject(aNode._connectionId); } else if (aNode.classList.contains("webconsole-msg-inspector")) { - this.pruneConsoleDirNode(aNode); - return; + let view = aNode._variablesView; + let actors = view ? + this.jsterm._objectActorsInVariablesViews.get(view) : + new Set(); + for (let actor of actors) { + this._releaseObject(actor); + } + actors.clear(); + aNode._variablesView = null; } if (aNode.parentNode) { aNode.parentNode.removeChild(aNode); } }, /** @@ -2110,35 +2040,34 @@ WebConsoleFrame.prototype = { spacer.flex = 1; iconContainer.appendChild(spacer); // Create the message body, which contains the actual text of the message. let bodyNode = this.document.createElementNS(XUL_NS, "description"); bodyNode.flex = 1; bodyNode.classList.add("webconsole-msg-body"); - // Store the body text, since it is needed later for the property tree - // case. + // Store the body text, since it is needed later for the variables view. let body = aBody; // If a string was supplied for the body, turn it into a DOM node and an // associated clipboard string now. aClipboardText = aClipboardText || (aBody + (aSourceURL ? " @ " + aSourceURL : "") + (aSourceLine ? ":" + aSourceLine : "")); // Create the containing node and append all its elements to it. let node = this.document.createElementNS(XUL_NS, "richlistitem"); if (aBody instanceof Ci.nsIDOMNode) { bodyNode.appendChild(aBody); } else { let str = undefined; if (aLevel == "dir") { - str = WebConsoleUtils.objectActorGripToString(aBody.arguments[0]); + str = VariablesView.getString(aBody.arguments[0]); } else if (["log", "info", "warn", "error", "debug"].indexOf(aLevel) > -1 && typeof aBody == "object") { this._makeConsoleLogMessageBody(node, bodyNode, aBody); } else { str = aBody; } @@ -2163,64 +2092,50 @@ WebConsoleFrame.prototype = { timestampNode.classList.add("webconsole-timestamp"); let timestamp = aTimeStamp || Date.now(); let timestampString = l10n.timestampString(timestamp); timestampNode.setAttribute("value", timestampString); // Create the source location (e.g. www.example.com:6) that sits on the // right side of the message, if applicable. let locationNode; - if (aSourceURL) { + if (aSourceURL && IGNORED_SOURCE_URLS.indexOf(aSourceURL) == -1) { locationNode = this.createLocationNode(aSourceURL, aSourceLine); } node.clipboardText = aClipboardText; node.classList.add("hud-msg-node"); node.timestamp = timestamp; this.setMessageType(node, aCategory, aSeverity); node.appendChild(timestampNode); node.appendChild(iconContainer); - // Display the object tree after the message node. + + // Display the variables view after the message node. if (aLevel == "dir") { - // Make the body container, which is a vertical box, for grouping the text - // and tree widgets. + let viewContainer = this.document.createElement("hbox"); + viewContainer.flex = 1; + viewContainer.height = this.outputNode.clientHeight * + CONSOLE_DIR_VIEW_HEIGHT; + + let options = { + objectActor: body.arguments[0], + targetElement: viewContainer, + hideFilterInput: true, + }; + this.jsterm.openVariablesView(options) + .then((aView) => node._variablesView = aView); + let bodyContainer = this.document.createElement("vbox"); bodyContainer.flex = 1; bodyContainer.appendChild(bodyNode); - // Create the tree. - let tree = this.document.createElement("tree"); - tree.setAttribute("hidecolumnpicker", "true"); - tree.flex = 1; - - let treecols = this.document.createElement("treecols"); - let treecol = this.document.createElement("treecol"); - treecol.setAttribute("primary", "true"); - treecol.setAttribute("hideheader", "true"); - treecol.setAttribute("ignoreincolumnpicker", "true"); - treecol.flex = 1; - treecols.appendChild(treecol); - tree.appendChild(treecols); - - tree.appendChild(this.document.createElement("treechildren")); - - bodyContainer.appendChild(tree); + bodyContainer.appendChild(viewContainer); node.appendChild(bodyContainer); node.classList.add("webconsole-msg-inspector"); - // Create the treeView object. - let treeView = node.propertyTreeView = new PropertyTreeView(); - - treeView.data = { - objectPropertiesProvider: this.objectPropertiesProvider.bind(this), - releaseObject: this._releaseObject.bind(this), - objectProperties: body.objectProperties, - }; - - tree.setAttribute("rows", treeView.rowCount); } else { node.appendChild(bodyNode); } node.appendChild(repeatContainer); if (locationNode) { node.appendChild(locationNode); } @@ -2258,53 +2173,46 @@ WebConsoleFrame.prototype = { configurable: false }); aBody.arguments.forEach(function(aItem) { if (aContainer.firstChild) { aContainer.appendChild(this.document.createTextNode(" ")); } - let text = WebConsoleUtils.objectActorGripToString(aItem); - - if (aItem && typeof aItem != "object" || !aItem.inspectable) { + let text = VariablesView.getString(aItem); + let inspectable = !VariablesView.isPrimitive({ value: aItem }); + + if (aItem && typeof aItem != "object" || !inspectable) { aContainer.appendChild(this.document.createTextNode(text)); - let longString = null; if (aItem.type == "longString") { - longString = aItem; - } - else if (!aItem.inspectable && - typeof aItem.displayString == "object" && - aItem.displayString.type == "longString") { - longString = aItem.displayString; - } - - if (longString) { let ellipsis = this.document.createElement("description"); ellipsis.classList.add("hud-clickable"); ellipsis.classList.add("longStringEllipsis"); ellipsis.textContent = l10n.getStr("longStringEllipsis"); + let formatter = function(s) '"' + s + '"'; + this._addMessageLinkCallback(ellipsis, - this._longStringClick.bind(this, aMessage, longString, null)); + this._longStringClick.bind(this, aMessage, aItem, formatter)); aContainer.appendChild(ellipsis); } return; } // For inspectable objects. let elem = this.document.createElement("description"); elem.classList.add("hud-clickable"); elem.setAttribute("aria-haspopup", "true"); elem.appendChild(this.document.createTextNode(text)); this._addMessageLinkCallback(elem, - this._consoleLogClick.bind(this, aMessage, elem, aItem)); + this._consoleLogClick.bind(this, elem, aItem)); aContainer.appendChild(elem); }, this); }, /** * Click event handler for the ellipsis shown immediately after a long string. * This method retrieves the full string and updates the console output to @@ -2677,28 +2585,76 @@ function JSTerm(aWebConsoleFrame) { this.hud = aWebConsoleFrame; this.hudId = this.hud.hudId; this.lastCompletion = { value: null }; this.history = []; this.historyIndex = 0; this.historyPlaceHolder = 0; // this.history.length; + this._objectActorsInVariablesViews = new Map(); + this._keyPress = this.keyPress.bind(this); this._inputEventHandler = this.inputEventHandler.bind(this); + this._fetchVarProperties = this._fetchVarProperties.bind(this); + this._fetchVarLongString = this._fetchVarLongString.bind(this); + + EventEmitter.decorate(this); } JSTerm.prototype = { + SELECTED_FRAME: -1, + /** * Stores the data for the last completion. * @type object */ lastCompletion: null, /** + * The Web Console sidebar. + * @see this._createSidebar() + * @see Sidebar.jsm + */ + sidebar: null, + + /** + * The Web Console splitter between output and the sidebar. + * @private + * @type nsIDOMElement + */ + _splitter: null, + + /** + * The Variables View instance shown in the sidebar. + * @private + * @type object + */ + _variablesView: null, + + /** + * Tells if you want the variables view UI updates to be lazy or not. Tests + * disable lazy updates. + * + * @private + * @type boolean + */ + _lazyVariablesView: true, + + /** + * Holds a map between VariablesView instances and sets of ObjectActor IDs + * that have been retrieved from the server. This allows us to release the + * objects when needed. + * + * @private + * @type Map + */ + _objectActorsInVariablesViews: null, + + /** * Last input value. * @type string */ lastInputValue: "", /** * History of code that was executed. * @type array @@ -2725,17 +2681,17 @@ JSTerm.prototype = { COMPLETE_BACKWARD: 1, COMPLETE_HINT_ONLY: 2, /** * Initialize the JSTerminal UI. */ init: function JST_init() { - let chromeDocument = this.hud.owner.chromeDocument; + let chromeDocument = this.hud.owner.chromeWindow.document; let autocompleteOptions = { onSelect: this.onAutocompleteSelect.bind(this), onClick: this.acceptProposedCompletion.bind(this), panelId: "webConsole_autocompletePopup", listBoxId: "webConsole_autocompletePopupListBox", position: "before_start", theme: "light", direction: "ltr", @@ -2746,16 +2702,18 @@ JSTerm.prototype = { let doc = this.hud.document; this.completeNode = doc.querySelector(".jsterm-complete-node"); this.inputNode = doc.querySelector(".jsterm-input-node"); this.inputNode.addEventListener("keypress", this._keyPress, false); this.inputNode.addEventListener("input", this._inputEventHandler, false); this.inputNode.addEventListener("keyup", this._inputEventHandler, false); + this._splitter = doc.querySelector(".devtools-side-splitter"); + this.lastInputValue && this.setInputValue(this.lastInputValue); }, /** * The JavaScript evaluation response handler. * * @private * @param nsIDOMElement [aAfterNode] @@ -2768,33 +2726,47 @@ JSTerm.prototype = { * The message received from the server. */ _executeResultCallback: function JST__executeResultCallback(aAfterNode, aCallback, aResponse) { if (!this.hud) { return; } - - let errorMessage = aResponse.errorMessage; + if (aResponse.error) { + Cu.reportError("Evaluation error " + aResponse.error + ": " + + aResponse.message); + return; + } + let errorMessage = aResponse.exceptionMessage; let result = aResponse.result; - let inspectable = result && typeof result == "object" && result.inspectable; + let inspectable = false; + if (result && !VariablesView.isPrimitive({ value: result })) { + inspectable = true; + } let helperResult = aResponse.helperResult; let helperHasRawOutput = !!(helperResult || {}).rawOutput; - let resultString = - WebConsoleUtils.objectActorGripToString(result, - !helperHasRawOutput); + let resultString = VariablesView.getString(result); if (helperResult && helperResult.type) { switch (helperResult.type) { case "clearOutput": this.clearOutput(); break; case "inspectObject": - this.handleInspectObject(helperResult.input, helperResult.object); + if (aAfterNode) { + if (!aAfterNode._objectActors) { + aAfterNode._objectActors = new Set(); + } + aAfterNode._objectActors.add(helperResult.object.actor); + } + this.openVariablesView({ + label: VariablesView.getString(helperResult.object), + objectActor: helperResult.object, + }); break; case "error": try { errorMessage = l10n.getStr(helperResult.message); } catch (ex) { errorMessage = helperResult.message; } @@ -2833,48 +2805,40 @@ JSTerm.prototype = { this._evalOutputClick.bind(this, aResponse), aAfterNode, aResponse.timestamp); } else { node = this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG, aAfterNode, aResponse.timestamp); } - if (result && typeof result == "object" && result.actor) { - node._objectActors = [result.actor]; - if (typeof result.displayString == "object" && - result.displayString.type == "longString") { - node._objectActors.push(result.displayString.actor); - } - - // Add an ellipsis to expand the short string if the object is not - // inspectable. - let longString = null; - let formatter = null; + node._objectActors = new Set(); + + let error = aResponse.exception; + if (WebConsoleUtils.isActorGrip(error)) { + node._objectActors.add(error.actor); + } + + if (WebConsoleUtils.isActorGrip(result)) { + node._objectActors.add(result.actor); + if (result.type == "longString") { - longString = result; - if (!helperHasRawOutput) { - formatter = WebConsoleUtils.formatResultString.bind(WebConsoleUtils); - } - } - else if (!inspectable && !errorMessage && - typeof result.displayString == "object" && - result.displayString.type == "longString") { - longString = result.displayString; - } - - if (longString) { + // Add an ellipsis to expand the short string if the object is not + // inspectable. + let body = node.querySelector(".webconsole-msg-body"); let ellipsis = this.hud.document.createElement("description"); ellipsis.classList.add("hud-clickable"); ellipsis.classList.add("longStringEllipsis"); ellipsis.textContent = l10n.getStr("longStringEllipsis"); - this.hud._addMessageLinkCallback(ellipsis, - this.hud._longStringClick.bind(this.hud, node, longString, formatter)); + let formatter = function(s) '"' + s + '"'; + let onclick = this.hud._longStringClick.bind(this.hud, node, result, + formatter); + this.hud._addMessageLinkCallback(ellipsis, onclick); body.appendChild(ellipsis); node.clipboardText += " " + ellipsis.textContent; } } }, @@ -2895,81 +2859,627 @@ JSTerm.prototype = { this.writeOutput(l10n.getStr("executeEmptyInput"), CATEGORY_OUTPUT, SEVERITY_LOG); return; } let node = this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG); let onResult = this._executeResultCallback.bind(this, node, aCallback); - this.webConsoleClient.evaluateJS(aExecuteString, onResult); + let options = { frame: this.SELECTED_FRAME }; + this.requestEvaluation(aExecuteString, options).then(onResult, onResult); this.history.push(aExecuteString); this.historyIndex++; this.historyPlaceHolder = this.history.length; this.setInputValue(""); this.clearCompletion(); }, /** - * Opens a new property panel that allows the inspection of the given object. - * The object information can be retrieved both async and sync, depending on - * the given options. + * Request a JavaScript string evaluation from the server. + * + * @param string aString + * String to execute. + * @param object [aOptions] + * Options for evaluation: + * - bindObjectActor: tells the ObjectActor ID for which you want to do + * the evaluation. The Debugger.Object of the OA will be bound to + * |_self| during evaluation, such that it's usable in the string you + * execute. + * - frame: tells the stackframe depth to evaluate the string in. If + * the jsdebugger is paused, you can pick the stackframe to be used for + * evaluation. Use |this.SELECTED_FRAME| to always pick the + * user-selected stackframe. + * If you do not provide a |frame| the string will be evaluated in the + * global content window. + * @return object + * A Promise object that is resolved when the server response is + * received. + */ + requestEvaluation: function JST_requestEvaluation(aString, aOptions = {}) + { + let deferred = Promise.defer(); + + function onResult(aResponse) { + if (!aResponse.error) { + deferred.resolve(aResponse); + } + else { + deferred.reject(aResponse); + } + } + + let frameActor = null; + if ("frame" in aOptions) { + frameActor = this.getFrameActor(aOptions.frame); + } + + let evalOptions = { + bindObjectActor: aOptions.bindObjectActor, + frameActor: frameActor, + }; + + this.webConsoleClient.evaluateJS(aString, onResult, evalOptions); + return deferred.promise; + }, + + /** + * Retrieve the FrameActor ID given a frame depth. + * + * @param number aFrame + * Frame depth. + * @return string|null + * The FrameActor ID for the given frame depth. + */ + getFrameActor: function JST_getFrameActor(aFrame) + { + let state = this.hud.owner.getDebuggerFrames(); + if (!state) { + return null; + } + + let grip; + if (aFrame == this.SELECTED_FRAME) { + grip = state.frames[state.selected]; + } + else { + grip = state.frames[aFrame]; + } + + return grip ? grip.actor : null; + }, + + /** + * Opens a new variables view that allows the inspection of the given object. * * @param object aOptions - * Property panel options: - * - title: - * Panel title. - * - anchor: - * The DOM element you want the panel to be anchored to. - * - updateButtonCallback: - * An optional function you want invoked when the user clicks the - * Update button. If this function is not provided the Update button is - * not shown. - * - data: - * An object that represents the object you want to inspect. Please see - * the PropertyPanel documentation - this object is passed to the - * PropertyPanel constructor + * Options for the variables view: + * - objectActor: grip of the ObjectActor you want to show in the + * variables view. + * - rawObject: the raw object you want to show in the variables view. + * - label: label to display in the variables view for inspected + * object. + * - hideFilterInput: optional boolean, |true| if you want to hide the + * variables view filter input. + * - targetElement: optional nsIDOMElement to append the variables view + * to. An iframe element is used as a container for the view. If this + * option is not used, then the variables view opens in the sidebar. + * @return object + * A Promise object that is resolved when the variables view has + * opened. The new variables view instance is given to the callbacks. + */ + openVariablesView: function JST_openVariablesView(aOptions) + { + let onContainerReady = (aWindow) => { + let container = aWindow.document.querySelector("#variables"); + let view = this._variablesView; + if (!view || aOptions.targetElement) { + let viewOptions = { + container: container, + hideFilterInput: aOptions.hideFilterInput, + }; + view = this._createVariablesView(viewOptions); + if (!aOptions.targetElement) { + this._variablesView = view; + } + } + aOptions.view = view; + this._updateVariablesView(aOptions); + this.emit("variablesview-open", view, aOptions); + return view; + }; + + let promise; + if (aOptions.targetElement) { + let deferred = Promise.defer(); + promise = deferred.promise; + let document = aOptions.targetElement.ownerDocument; + let iframe = document.createElement("iframe"); + + iframe.addEventListener("load", function onIframeLoad(aEvent) { + iframe.removeEventListener("load", onIframeLoad, true); + deferred.resolve(iframe.contentWindow); + }, true); + + iframe.flex = 1; + iframe.setAttribute("src", VARIABLES_VIEW_URL); + aOptions.targetElement.appendChild(iframe); + } + else { + this._createSidebar(); + promise = this._addVariablesViewSidebarTab(); + } + + return promise.then(onContainerReady); + }, + + /** + * Create the Web Console sidebar. + * + * @see Sidebar.jsm + * @private + */ + _createSidebar: function JST__createSidebar() + { + if (!this.sidebar) { + let tabbox = this.hud.document.querySelector("#webconsole-sidebar"); + this.sidebar = new ToolSidebar(tabbox, this); + } + this.sidebar.show(); + this._splitter.setAttribute("state", "open"); + }, + + /** + * Add the variables view tab to the sidebar. + * + * @private + * @return object + * A Promise object for the adding of the new tab. + */ + _addVariablesViewSidebarTab: function JST__addVariablesViewSidebarTab() + { + let deferred = Promise.defer(); + + let onTabReady = () => { + let window = this.sidebar.getWindowForTab("variablesview"); + deferred.resolve(window); + }; + + let tab = this.sidebar.getTab("variablesview"); + if (tab) { + if (this.sidebar.getCurrentTabID() == "variablesview") { + onTabReady(); + } + else { + this.sidebar.once("variablesview-selected", onTabReady); + this.sidebar.select("variablesview"); + } + } + else { + this.sidebar.once("variablesview-ready", onTabReady); + this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true); + } + + return deferred.promise; + }, + + /** + * Create a variables view instance. + * + * @private + * @param object aOptions + * Options for the new Variables View instance: + * - container: the DOM element where the variables view is inserted. + * - hideFilterInput: boolean, if true the variables filter input is + * hidden. * @return object - * The new instance of PropertyPanel. + * The new Variables View instance. + */ + _createVariablesView: function JST__createVariablesView(aOptions) + { + let view = new VariablesView(aOptions.container); + view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder"); + view.emptyText = l10n.getStr("emptyPropertiesList"); + view.searchEnabled = !aOptions.hideFilterInput; + view.lazyEmpty = this._lazyVariablesView; + view.lazyAppend = this._lazyVariablesView; + this._objectActorsInVariablesViews.set(view, new Set()); + return view; + }, + + /** + * Update the variables view. + * + * @private + * @param object aOptions + * Options for updating the variables view: + * - view: the view you want to update. + * - objectActor: the grip of the new ObjectActor you want to show in + * the view. + * - rawObject: the new raw object you want to show. + * - label: the new label for the inspected object. */ - openPropertyPanel: function JST_openPropertyPanel(aOptions) + _updateVariablesView: function JST__updateVariablesView(aOptions) + { + let view = aOptions.view; + view.createHierarchy(); + view.empty(); + + let actors = this._objectActorsInVariablesViews.get(view); + for (let actor of actors) { + // We need to avoid pruning the object inspection starting point. + // That one is pruned when the console message is removed. + if (view._consoleLastObjectActor != actor) { + this.hud._releaseObject(actor); + } + } + + actors.clear(); + + if (aOptions.objectActor) { + // Make sure eval works in the correct context. + view.eval = this._variablesViewEvaluate.bind(this, aOptions); + view.switch = this._variablesViewSwitch.bind(this, aOptions); + view.delete = this._variablesViewDelete.bind(this, aOptions); + } + else { + view.eval = null; + view.switch = null; + view.delete = null; + } + + let scope = view.addScope(aOptions.label); + scope.expanded = true; + scope.locked = true; + + let container = scope.addVar(); + container.evaluationMacro = this._variablesViewSimpleValueEvalMacro; + + if (aOptions.objectActor) { + this._fetchVarProperties(container, aOptions.objectActor); + view._consoleLastObjectActor = aOptions.objectActor.actor; + } + else if (aOptions.rawObject) { + container.populate(aOptions.rawObject); + view.commitHierarchy(); + view._consoleLastObjectActor = null; + } + else { + throw new Error("Variables View cannot open without giving it an object " + + "display."); + } + + this.emit("variablesview-updated", view, aOptions); + }, + + /** + * The evaluation function used by the variables view when editing a property + * value. + * + * @private + * @param object aOptions + * The options used for |this._updateVariablesView()|. + * @param string aString + * The string that the variables view wants to evaluate. + */ + _variablesViewEvaluate: function JST__variablesViewEvaluate(aOptions, aString) + { + let updater = this._updateVariablesView.bind(this, aOptions); + let onEval = this._silentEvalCallback.bind(this, updater); + + let evalOptions = { + frame: this.SELECTED_FRAME, + bindObjectActor: aOptions.objectActor.actor, + }; + + this.requestEvaluation(aString, evalOptions).then(onEval, onEval); + }, + + /** + * Generates the string evaluated when performing simple value changes in the + * variables view. + * + * @private + * @param Variable | Property aItem + * The current variable or property. + * @param string aCurrentString + * The trimmed user inputted string. + * @return string + * The string to be evaluated. + */ + _variablesViewSimpleValueEvalMacro: + function JST__variablesViewSimpleValueEvalMacro(aItem, aCurrentString) + { + return "_self" + aItem.symbolicName + "=" + aCurrentString; + }, + + + /** + * Generates the string evaluated when overriding getters and setters with + * plain values in the variables view. + * + * @private + * @param Property aItem + * The current getter or setter property. + * @param string aCurrentString + * The trimmed user inputted string. + * @return string + * The string to be evaluated. + */ + _variablesViewOverrideValueEvalMacro: + function JST__variablesViewOverrideValueEvalMacro(aItem, aCurrentString) { - // The property panel has one button: - // `Update`: reexecutes the string executed on the command line. The - // result will be inspected by this panel. - let buttons = []; - - if (aOptions.updateButtonCallback) { - buttons.push({ - label: l10n.getStr("update.button"), - accesskey: l10n.getStr("update.accesskey"), - oncommand: aOptions.updateButtonCallback, - }); + let parent = aItem.ownerView; + let symbolicName = parent.symbolicName; + if (symbolicName.indexOf("_self") != 0) { + parent._symbolicName = "_self" + symbolicName; + } + + let result = VariablesView.overrideValueEvalMacro.apply(this, arguments); + + parent._symbolicName = symbolicName; + + return result; + }, + + /** + * Generates the string evaluated when performing getters and setters changes + * in the variables view. + * + * @private + * @param Property aItem + * The current getter or setter property. + * @param string aCurrentString + * The trimmed user inputted string. + * @return string + * The string to be evaluated. + */ + _variablesViewGetterOrSetterEvalMacro: + function JST__variablesViewGetterOrSetterEvalMacro(aItem, aCurrentString) + { + let propertyObject = aItem.ownerView; + let parentObject = propertyObject.ownerView; + let parent = parentObject.symbolicName; + parentObject._symbolicName = "_self" + parent; + + let result = VariablesView.getterOrSetterEvalMacro.apply(this, arguments); + + parentObject._symbolicName = parent; + + return result; + }, + + /** + * The property deletion function used by the variables view when a property + * is deleted. + * + * @private + * @param object aOptions + * The options used for |this._updateVariablesView()|. + * @param object aVar + * The Variable object instance for the deleted property. + */ + _variablesViewDelete: function JST__variablesViewDelete(aOptions, aVar) + { + let onEval = this._silentEvalCallback.bind(this, null); + + let evalOptions = { + frame: this.SELECTED_FRAME, + bindObjectActor: aOptions.objectActor.actor, + }; + + this.requestEvaluation("delete _self" + aVar.symbolicName, evalOptions) + .then(onEval, onEval); + }, + + /** + * The property rename function used by the variables view when a property + * is renamed. + * + * @private + * @param object aOptions + * The options used for |this._updateVariablesView()|. + * @param object aVar + * The Variable object instance for the renamed property. + * @param string aNewName + * The new name for the property. + */ + _variablesViewSwitch: + function JST__variablesViewSwitch(aOptions, aVar, aNewName) + { + let updater = this._updateVariablesView.bind(this, aOptions); + let onEval = this._silentEvalCallback.bind(this, updater); + + let evalOptions = { + frame: this.SELECTED_FRAME, + bindObjectActor: aOptions.objectActor.actor, + }; + + let newSymbolicName = aVar.ownerView.symbolicName + '["' + aNewName + '"]'; + if (newSymbolicName == aVar.symbolicName) { + return; + } + + let code = "_self" + newSymbolicName + " = _self" + aVar.symbolicName + ";" + + "delete _self" + aVar.symbolicName; + + this.requestEvaluation(code, evalOptions).then(onEval, onEval); + }, + + /** + * A noop callback for JavaScript evaluation. This method releases any + * result ObjectActors that come from the server for evaluation requests. This + * is used for editing, renaming and deleting properties in the variables + * view. + * + * Exceptions are displayed in the output. + * + * @private + * @param function aCallback + * Function to invoke once the response is received. + * @param object aResponse + * The response packet received from the server. + */ + _silentEvalCallback: function JST__silentEvalCallback(aCallback, aResponse) + { + if (aResponse.error) { + Cu.reportError("Web Console evaluation failed. " + aResponse.error + ":" + + aResponse.message); + + aCallback && aCallback(aResponse); + return; + } + + let exception = aResponse.exception; + if (exception) { + let node = this.writeOutput(aResponse.exceptionMessage, + CATEGORY_OUTPUT, SEVERITY_ERROR, + null, aResponse.timestamp); + node._objectActors = new Set(); + if (WebConsoleUtils.isActorGrip(exception)) { + node._objectActors.add(exception.actor); + } } - let parent = this.hud.popupset; - let title = aOptions.title ? - l10n.getFormatStr("jsPropertyInspectTitle", [aOptions.title]) : - l10n.getStr("jsPropertyTitle"); - - let propPanel = new PropertyPanel(parent, title, aOptions.data, buttons); - - propPanel.panel.openPopup(aOptions.anchor, "after_pointer", 0, 0, false, false); - propPanel.panel.sizeTo(350, 450); - - if (aOptions.anchor) { - propPanel.panel.addEventListener("popuphiding", function onPopupHide() { - propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); - aOptions.anchor._panelOpen = false; - }, false); - aOptions.anchor._panelOpen = true; + let helper = aResponse.helperResult || { type: null }; + let helperGrip = null; + if (helper.type == "inspectObject") { + helperGrip = helper.object; + } + + let grips = [aResponse.result, helperGrip]; + for (let grip of grips) { + if (WebConsoleUtils.isActorGrip(grip)) { + this.hud._releaseObject(grip.actor); + } + } + + aCallback && aCallback(aResponse); + }, + + /** + * Adds properties to a variable in the view. Triggered when a variable is + * expanded. It does not expand the variable. + * + * @param object aVar + * The VariablseView Variable instance where the properties get added. + * @param object [aGrip] + * Optional, the object actor grip of the variable. If the grip is not + * provided, then the aVar.value is used as the object actor grip. + */ + _fetchVarProperties: function JST__fetchVarProperties(aVar, aGrip) + { + // Retrieve the properties only once. + if (aVar._fetched) { + return; + } + aVar._fetched = true; + + let grip = aGrip || aVar.value; + if (!grip) { + throw new Error("No object actor grip was given for the variable."); + } + + let view = aVar._variablesView; + let actors = this._objectActorsInVariablesViews.get(view); + + function addActorForDescriptor(aGrip) { + if (WebConsoleUtils.isActorGrip(aGrip)) { + actors.add(aGrip.actor); + } } - return propPanel; + let onNewProperty = (aProperty) => { + if (aProperty.getter || aProperty.setter) { + aProperty.evaluationMacro = this._variablesViewOverrideValueEvalMacro; + let getter = aProperty.get("get"); + let setter = aProperty.get("set"); + if (getter) { + getter.evaluationMacro = this._variablesViewGetterOrSetterEvalMacro; + } + if (setter) { + setter.evaluationMacro = this._variablesViewGetterOrSetterEvalMacro; + } + } + else { + aProperty.evaluationMacro = this._variablesViewSimpleValueEvalMacro; + } + + let grips = [aProperty.value, aProperty.gettter, aProperty.settter]; + grips.forEach(addActorForDescriptor); + + let inspectable = !VariablesView.isPrimitive({ value: aProperty.value }); + let longString = WebConsoleUtils.isActorGrip(aProperty.value) && + aProperty.value.type == "longString"; + if (inspectable) { + aProperty.onexpand = this._fetchVarProperties; + } + else if (longString) { + aProperty.onexpand = this._fetchVarLongString; + aProperty.showArrow(); + } + }; + + let client = new GripClient(this.hud.proxy.client, grip); + client.getPrototypeAndProperties((aResponse) => { + let { ownProperties, prototype } = aResponse; + let sortable = VariablesView.NON_SORTABLE_CLASSES.indexOf(grip.class) == -1; + + // Add all the variable properties. + if (ownProperties) { + aVar.addProperties(ownProperties, { + sorted: sortable, + callback: onNewProperty, + }); + } + + // Add the variable's __proto__. + if (prototype && prototype.type != "null") { + let proto = aVar.addProperty("__proto__", { value: prototype }); + onNewProperty(proto); + } + + aVar._retrieved = true; + view.commitHierarchy(); + this.emit("variablesview-fetched", aVar); + }); + }, + + /** + * Fetch the full string for a given variable that displays a long string. + * + * @param object aVar + * The VariablesView Variable instance where the properties get added. + */ + _fetchVarLongString: function JST__fetchVarLongString(aVar) + { + if (aVar._fetched) { + return; + } + aVar._fetched = true; + + let grip = aVar.value; + if (!grip) { + throw new Error("No long string actor grip was given for the variable."); + } + + let client = this.webConsoleClient.longString(grip); + client.substring(grip.initial.length, grip.length, (aResponse) => { + if (aResponse.error) { + Cu.reportError("JST__fetchVarLongString substring failure: " + + aResponse.error + ": " + aResponse.message); + return; + } + + aVar.onexpand = null; + aVar.setGrip(grip.initial + aResponse.substring); + aVar.hideArrow(); + aVar._retrieved = true; + }); }, /** * Writes a JS object to the JSTerm outputNode. * * @param string aOutputMessage * The message to display. * @param function [aCallback] @@ -3593,178 +4103,56 @@ JSTerm.prototype = { updateCompleteNode: function JSTF_updateCompleteNode(aSuffix) { // completion prefix = input, with non-control chars replaced by spaces let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : ""; this.completeNode.value = prefix + aSuffix; }, /** - * The JSTerm InspectObject remote message handler. This allows the remote - * process to open the Property Panel for a given object. - * - * @param object aRequest - * The request message from the content process. This message includes - * the user input string that was evaluated to inspect an object and - * the result object which is to be inspected. - */ - handleInspectObject: function JST_handleInspectObject(aInput, aActor) - { - let options = { - title: aInput, - - data: { - objectPropertiesProvider: this.hud.objectPropertiesProvider.bind(this.hud), - releaseObject: this.hud._releaseObject.bind(this.hud), - }, - }; - - let propPanel; - - let onPopupHide = function JST__onPopupHide() { - propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); - this.hud._releaseObject(aActor.actor); - }.bind(this); - - this.hud.objectPropertiesProvider(aActor.actor, - function _onObjectProperties(aProperties) { - options.data.objectProperties = aProperties; - propPanel = this.openPropertyPanel(options); - propPanel.panel.setAttribute("hudId", this.hudId); - propPanel.panel.addEventListener("popuphiding", onPopupHide, false); - }.bind(this)); - }, - - /** * The click event handler for evaluation results in the output. * * @private * @param object aResponse * The JavaScript evaluation response received from the server. - * @param nsIDOMNode aLink - * The message node for which we are handling events. */ - _evalOutputClick: function JST__evalOutputClick(aResponse, aLinkNode) + _evalOutputClick: function JST__evalOutputClick(aResponse) { - if (aLinkNode._panelOpen) { - return; - } - - let options = { - title: aResponse.input, - anchor: aLinkNode, - - // Data to inspect. - data: { - objectPropertiesProvider: this.hud.objectPropertiesProvider.bind(this.hud), - releaseObject: this.hud._releaseObject.bind(this.hud), - }, - }; - - let propPanel; - - options.updateButtonCallback = function JST__evalUpdateButton() { - let onResult = - this._evalOutputUpdatePanelCallback.bind(this, options, propPanel, - aResponse); - this.webConsoleClient.evaluateJS(aResponse.input, onResult); - }.bind(this); - - let onPopupHide = function JST__evalInspectPopupHide() { - propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); - - if (!aLinkNode.parentNode && aLinkNode._objectActors) { - aLinkNode._objectActors.forEach(this.hud._releaseObject, this.hud); - aLinkNode._objectActors = null; - } - }.bind(this); - - this.hud.objectPropertiesProvider(aResponse.result.actor, - function _onObjectProperties(aProperties) { - options.data.objectProperties = aProperties; - propPanel = this.openPropertyPanel(options); - propPanel.panel.setAttribute("hudId", this.hudId); - propPanel.panel.addEventListener("popuphiding", onPopupHide, false); - }.bind(this)); - }, - - /** - * The callback used for updating the Property Panel when the user clicks the - * Update button. - * - * @private - * @param object aOptions - * The options object used for opening the initial Property Panel. - * @param object aPropPanel - * The Property Panel instance. - * @param object aOldResponse - * The previous JSTerm:EvalResult message received from the content - * process. - * @param object aNewResponse - * The new JSTerm:EvalResult message received after the user clicked - * the Update button. - */ - _evalOutputUpdatePanelCallback: - function JST__updatePanelCallback(aOptions, aPropPanel, aOldResponse, - aNewResponse) - { - if (aNewResponse.errorMessage) { - this.writeOutput(aNewResponse.errorMessage, CATEGORY_OUTPUT, - SEVERITY_ERROR); - return; - } - - let result = aNewResponse.result; - let inspectable = result && typeof result == "object" && result.inspectable; - let newActor = result && typeof result == "object" ? result.actor : null; - - let anchor = aOptions.anchor; - if (anchor && newActor) { - if (!anchor._objectActors) { - anchor._objectActors = []; - } - if (anchor._objectActors.indexOf(newActor) == -1) { - anchor._objectActors.push(newActor); - } - } - - if (!inspectable) { - this.writeOutput(l10n.getStr("JSTerm.updateNotInspectable"), CATEGORY_OUTPUT, SEVERITY_ERROR); - return; - } - - // Update the old response object such that when the panel is reopen, the - // user sees the new response. - aOldResponse.result = aNewResponse.result; - aOldResponse.error = aNewResponse.error; - aOldResponse.errorMessage = aNewResponse.errorMessage; - aOldResponse.timestamp = aNewResponse.timestamp; - - this.hud.objectPropertiesProvider(newActor, - function _onObjectProperties(aProperties) { - aOptions.data.objectProperties = aProperties; - // TODO: This updates the value of the tree. - // However, the states of open nodes is not saved. - // See bug 586246. - aPropPanel.treeView.data = aOptions.data; - }.bind(this)); + this.openVariablesView({ + label: VariablesView.getString(aResponse.result), + objectActor: aResponse.result, + }); }, /** * Destroy the JSTerm object. Call this method to avoid memory leaks. */ destroy: function JST_destroy() { + if (this._variablesView) { + let actors = this._objectActorsInVariablesViews.get(this._variablesView); + for (let actor of actors) { + this.hud._releaseObject(actor); + } + actors.clear(); + this._variablesView = null; + } + + if (this.sidebar) { + this.sidebar.destroy(); + this.sidebar = null; + } + this.clearCompletion(); this.clearOutput(); this.autocompletePopup.destroy(); this.autocompletePopup = null; - let popup = this.hud.owner.chromeDocument + let popup = this.hud.owner.chromeWindow.document .getElementById("webConsole_autocompletePopup"); if (popup) { popup.parentNode.removeChild(popup); } this.inputNode.removeEventListener("keypress", this._keyPress, false); this.inputNode.removeEventListener("input", this._inputEventHandler, false); this.inputNode.removeEventListener("keyup", this._inputEventHandler, false); @@ -3932,16 +4320,18 @@ CommandController.prototype = { let selectedItem = this.owner.outputNode.selectedItem; return selectedItem && selectedItem.url; } case "cmd_fontSizeEnlarge": case "cmd_fontSizeReduce": case "cmd_fontSizeReset": case "cmd_selectAll": return true; + case "cmd_close": + return this.owner.owner._browserConsole; } }, doCommand: function CommandController_doCommand(aCommand) { switch (aCommand) { case "cmd_copy": this.copy(); @@ -3959,16 +4349,19 @@ CommandController.prototype = { this.owner.changeFontSize("+"); break; case "cmd_fontSizeReduce": this.owner.changeFontSize("-"); break; case "cmd_fontSizeReset": this.owner.changeFontSize(""); break; + case "cmd_close": + this.owner.window.close(); + break; } } }; /////////////////////////////////////////////////////////////////////////////// // Web Console connection proxy ///////////////////////////////////////////////////////////////////////////////
--- a/browser/devtools/webconsole/webconsole.xul +++ b/browser/devtools/webconsole/webconsole.xul @@ -8,18 +8,23 @@ ]> <?xml-stylesheet href="chrome://global/skin/" type="text/css"?> <?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?> <?xml-stylesheet href="chrome://browser/skin/devtools/webconsole.css" type="text/css"?> <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="devtools-webconsole" + macanimationtype="document" + fullscreenbutton="true" title="&window.title;" + browserConsoleTitle="&browserConsole.title;" windowtype="devtools:webconsole" + width="900" height="350" persist="screenX screenY width height sizemode"> <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/> <script type="text/javascript" src="webconsole.js"/> <commandset id="editMenuCommands"/> <commandset id="consoleCommands" commandupdater="true" @@ -27,25 +32,27 @@ oncommandupdate="goUpdateConsoleCommands();"> <command id="consoleCmd_openURL" oncommand="goDoCommand('consoleCmd_openURL');"/> <command id="consoleCmd_copyURL" oncommand="goDoCommand('consoleCmd_copyURL');"/> <command id="cmd_fullZoomEnlarge" oncommand="goDoCommand('cmd_fontSizeEnlarge');"/> <command id="cmd_fullZoomReduce" oncommand="goDoCommand('cmd_fontSizeReduce');"/> <command id="cmd_fullZoomReset" oncommand="goDoCommand('cmd_fontSizeReset');"/> + <command id="cmd_close" oncommand="goDoCommand('cmd_close');"/> </commandset> - <keyset id="fontSizeChangeSet"> + <keyset id="consoleKeys"> <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/> <key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/> <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/> <key key="&fullZoomEnlargeCmd.commandkey2;" command="cmd_fullZoomEnlarge" modifiers="accel"/> <key key="&fullZoomEnlargeCmd.commandkey3;" command="cmd_fullZoomEnlarge" modifiers="accel"/> <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;" command="cmd_fullZoomReset" modifiers="accel"/> <key key="&fullZoomResetCmd.commandkey2;" command="cmd_fullZoomReset" modifiers="accel"/> + <key key="&closeCmd.key;" command="cmd_close" modifiers="accel"/> </keyset> <keyset id="editMenuKeys"/> <popupset id="mainPopupSet"> <menupopup id="output-contextmenu" onpopupshowing="ConsoleContextMenu.build(event);"> <menuitem id="saveBodiesContextMenu" type="checkbox" label="&saveBodies.label;" accesskey="&saveBodies.accesskey;"/> @@ -55,17 +62,17 @@ <menuitem id="menu_copyURL" label="©URLCmd.label;" accesskey="©URLCmd.accesskey;" command="consoleCmd_copyURL" selection="network" selectionType="single"/> <menuitem id="menu_copy"/> <menuitem id="menu_selectAll"/> </menupopup> </popupset> - <vbox class="hud-outer-wrapper" flex="1"> + <hbox class="hud-outer-wrapper" flex="1"> <vbox class="hud-console-wrapper" flex="1"> <toolbar class="hud-console-filter-toolbar devtools-toolbar" mode="full"> <toolbarbutton label="&btnPageNet.label;" type="menu-button" category="net" class="devtools-toolbarbutton webconsole-filter-button" tooltiptext="&btnPageNet.tooltip;"> <menupopup> <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false" prefKey="network"/> @@ -109,25 +116,34 @@ <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false" prefKey="log"/> </menupopup> </toolbarbutton> <spacer flex="1"/> <textbox class="compact hud-filter-box devtools-searchinput" type="search" - placeholder="&filterBox.placeholder;"/> + placeholder="&filterOutput.placeholder;"/> <toolbarbutton class="webconsole-clear-console-button devtools-toolbarbutton" label="&btnClear.label;" tooltiptext="&btnClear.tooltip;"/> </toolbar> + <richlistbox class="hud-output-node" orient="vertical" flex="1" seltype="multiple" context="output-contextmenu" style="direction:ltr;"/> + <hbox class="jsterm-input-container" style="direction:ltr"> <stack class="jsterm-stack-node" flex="1"> <textbox class="jsterm-complete-node" multiline="true" rows="1" tabindex="-1"/> <textbox class="jsterm-input-node" multiline="true" rows="1"/> </stack> </hbox> </vbox> - </vbox> + + <splitter class="devtools-side-splitter" collapse="after" state="collapsed" /> + + <tabbox id="webconsole-sidebar" class="devtools-sidebar-tabs" hidden="true" width="300"> + <tabs/> + <tabpanels flex="1"/> + </tabbox> + </hbox> </window>
--- a/browser/locales/en-US/chrome/browser/browser.dtd +++ b/browser/locales/en-US/chrome/browser/browser.dtd @@ -202,16 +202,18 @@ These should match what Safari and other <!ENTITY devtoolsConnect.accesskey "e"> <!ENTITY errorConsoleCmd.label "Error Console"> <!ENTITY errorConsoleCmd.accesskey "C"> <!ENTITY errorConsoleCmd.commandkey "j"> <!ENTITY remoteWebConsoleCmd.label "Remote Web Console"> +<!ENTITY browserConsoleCmd.label "Browser Console"> + <!ENTITY inspectContextMenu.label "Inspect Element"> <!ENTITY inspectContextMenu.accesskey "Q"> <!ENTITY responsiveDesignTool.label "Responsive Design View"> <!ENTITY responsiveDesignTool.accesskey "R"> <!ENTITY responsiveDesignTool.commandkey "M"> <!-- LOCALIZATION NOTE (scratchpad.label): This menu item label appears
new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/devtools/VariablesView.dtd @@ -0,0 +1,12 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!-- LOCALIZATION NOTE : FILE 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. --> + +<!ENTITY PropertiesViewWindowTitle "Properties"> +
--- 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/locales/en-US/chrome/browser/devtools/netmonitor.properties +++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties @@ -85,14 +85,14 @@ requestCookies=Request cookies # in the network details params tab identifying the response cookies. responseCookies=Response cookies # LOCALIZATION NOTE (jsonFilterText): This is the text displayed # in the response tab of the network details pane for the JSON filtering input. jsonFilterText=Filter properties # LOCALIZATION NOTE (networkMenu.size): This is the label displayed -# in the network menu specifying the size of a request (in kb). -networkMenu.size=%Skb +# in the network menu specifying the size of a request (in kilobytes). +networkMenu.sizeKB=%S KB # LOCALIZATION NOTE (networkMenu.total): This is the label displayed -# in the network menu specifying the time for a request to finish (in ms). -networkMenu.total=→ %Sms +# in the network menu specifying the time for a request to finish (in milliseconds). +networkMenu.totalMS=→ %S ms
--- a/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd @@ -4,16 +4,17 @@ <!-- LOCALIZATION NOTE : FILE 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. --> <!ENTITY window.title "Web Console"> +<!ENTITY browserConsole.title "Browser Console"> <!ENTITY networkPanel.requestURLColon "Request URL:"> <!ENTITY networkPanel.requestMethodColon "Request Method:"> <!ENTITY networkPanel.statusCodeColon "Status Code:"> <!ENTITY networkPanel.requestHeaders "Request Headers"> <!ENTITY networkPanel.requestCookie "Sent Cookie"> <!ENTITY networkPanel.requestBody "Request Body"> @@ -55,24 +56,26 @@ - console.error(). --> <!ENTITY btnPageLogging.label "Logging"> <!ENTITY btnPageLogging.tooltip "Log messages sent to the window.console object"> <!ENTITY btnConsoleErrors "Errors"> <!ENTITY btnConsoleInfo "Info"> <!ENTITY btnConsoleWarnings "Warnings"> <!ENTITY btnConsoleLog "Log"> -<!ENTITY filterBox.placeholder "Filter"> +<!ENTITY filterOutput.placeholder "Filter output"> <!ENTITY btnClear.label "Clear"> <!ENTITY btnClear.tooltip "Clear the Web Console output"> <!ENTITY fullZoomEnlargeCmd.commandkey "+"> <!ENTITY fullZoomEnlargeCmd.commandkey2 "="> <!-- + is above this key on many keyboards --> <!ENTITY fullZoomEnlargeCmd.commandkey3 ""> <!ENTITY fullZoomReduceCmd.commandkey "-"> <!ENTITY fullZoomReduceCmd.commandkey2 ""> <!ENTITY fullZoomResetCmd.commandkey "0"> <!ENTITY fullZoomResetCmd.commandkey2 ""> <!ENTITY copyURLCmd.label "Copy Link Location"> <!ENTITY copyURLCmd.accesskey "a"> + +<!ENTITY closeCmd.key "W">
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties +++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties @@ -192,8 +192,17 @@ NetworkPanel.fetchRemainingResponseConte # LOCALIZATION NOTE (NetworkPanel.fetchRemainingRequestContentLink): This is # displayed in the network panel when the request body is only partially # available. NetworkPanel.fetchRemainingRequestContentLink=Fetch the request body (%1$S bytes) # LOCALIZATION NOTE (connectionTimeout): Message displayed when the Remote Web # Console fails to connect to the server due to a timeout. connectionTimeout=Connection timeout. Check the Error Console on both ends for potential error messages. Reopen the Web Console to try again. + +# LOCALIZATION NOTE (propertiesFilterPlaceholder): This is the text that +# appears in the filter text box for the properties view container. +propertiesFilterPlaceholder=Filter properties + +# LOCALIZATION NOTE (emptyPropertiesList): The text that is displayed in the +# properties pane when there are no properties to display. +emptyPropertiesList=No properties to display +
--- a/browser/locales/jar.mn +++ b/browser/locales/jar.mn @@ -33,16 +33,17 @@ locale/browser/devtools/tilt.properties (%chrome/browser/devtools/tilt.properties) locale/browser/devtools/scratchpad.properties (%chrome/browser/devtools/scratchpad.properties) locale/browser/devtools/scratchpad.dtd (%chrome/browser/devtools/scratchpad.dtd) locale/browser/devtools/styleeditor.properties (%chrome/browser/devtools/styleeditor.properties) locale/browser/devtools/styleeditor.dtd (%chrome/browser/devtools/styleeditor.dtd) locale/browser/devtools/styleinspector.properties (%chrome/browser/devtools/styleinspector.properties) locale/browser/devtools/styleinspector.dtd (%chrome/browser/devtools/styleinspector.dtd) locale/browser/devtools/webConsole.dtd (%chrome/browser/devtools/webConsole.dtd) + locale/browser/devtools/VariablesView.dtd (%chrome/browser/devtools/VariablesView.dtd) locale/browser/devtools/sourceeditor.properties (%chrome/browser/devtools/sourceeditor.properties) locale/browser/devtools/sourceeditor.dtd (%chrome/browser/devtools/sourceeditor.dtd) locale/browser/devtools/profiler.dtd (%chrome/browser/devtools/profiler.dtd) locale/browser/devtools/profiler.properties (%chrome/browser/devtools/profiler.properties) locale/browser/devtools/layoutview.dtd (%chrome/browser/devtools/layoutview.dtd) locale/browser/devtools/responsiveUI.properties (%chrome/browser/devtools/responsiveUI.properties) locale/browser/devtools/toolbox.dtd (%chrome/browser/devtools/toolbox.dtd) locale/browser/devtools/toolbox.properties (%chrome/browser/devtools/toolbox.properties)
--- 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/linux/devtools/netmonitor.css +++ b/browser/themes/linux/devtools/netmonitor.css @@ -130,20 +130,20 @@ transparent 75%); } .requests-menu-timings { transform-origin: left center; } .requests-menu-timings-total { - -moz-padding-start: 4px; + -moz-padding-start: 8px; font-size: 85%; font-weight: 600; - transform-origin: -4px center; /* negative cap size */ + transform-origin: left center; } .requests-menu-timings-cap { width: 4px; height: 10px; border: 1px solid #fff; }
--- a/browser/themes/linux/devtools/webconsole.css +++ b/browser/themes/linux/devtools/webconsole.css @@ -234,8 +234,23 @@ :-moz-any(.jsterm-input-node, .jsterm-complete-node) > .textbox-input-box > .textbox-textarea { overflow-x: hidden; } .jsterm-complete-node > .textbox-input-box > .textbox-textarea { color: GrayText; } + +.webconsole-msg-inspector iframe { + height: 7em; + margin-bottom: 15px; +} + +.devtools-side-splitter { + background: #666; + width: 2px; +} + +#webconsole-sidebar > tabs { + height: 0; + overflow: hidden; +}
--- 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/osx/devtools/netmonitor.css +++ b/browser/themes/osx/devtools/netmonitor.css @@ -130,20 +130,20 @@ transparent 75%); } .requests-menu-timings { transform-origin: left center; } .requests-menu-timings-total { - -moz-padding-start: 4px; + -moz-padding-start: 8px; font-size: 85%; font-weight: 600; - transform-origin: -4px center; /* negative cap size */ + transform-origin: left center; } .requests-menu-timings-cap { width: 4px; height: 10px; border: 1px solid #fff; }
--- a/browser/themes/osx/devtools/webconsole.css +++ b/browser/themes/osx/devtools/webconsole.css @@ -238,8 +238,23 @@ :-moz-any(.jsterm-input-node, .jsterm-complete-node) > .textbox-input-box > .textbox-textarea { overflow-x: hidden; } .jsterm-complete-node > .textbox-input-box > .textbox-textarea { color: GrayText; } + +.webconsole-msg-inspector iframe { + height: 7em; + margin-bottom: 15px; +} + +.devtools-side-splitter { + background: #666; + width: 2px; +} + +#webconsole-sidebar > tabs { + height: 0; + overflow: hidden; +}
--- 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/browser/themes/windows/devtools/netmonitor.css +++ b/browser/themes/windows/devtools/netmonitor.css @@ -130,20 +130,20 @@ transparent 75%); } .requests-menu-timings { transform-origin: left center; } .requests-menu-timings-total { - -moz-padding-start: 4px; + -moz-padding-start: 8px; font-size: 85%; font-weight: 600; - transform-origin: -4px center; /* negative cap size */ + transform-origin: left center; } .requests-menu-timings-cap { width: 4px; height: 10px; border: 1px solid #fff; }
--- a/browser/themes/windows/devtools/webconsole.css +++ b/browser/themes/windows/devtools/webconsole.css @@ -242,8 +242,23 @@ /* * This hardcoded width likely due to a toolkit Windows specific bug. * See http://hg.mozilla.org/mozilla-central/annotate/f38d6df93cad/toolkit/themes/winstripe/global/textbox-aero.css#l7 */ .hud-filter-box { width: 200px; } + +.webconsole-msg-inspector iframe { + height: 7em; + margin-bottom: 15px; +} + +.devtools-side-splitter { + background: #666; + width: 2px; +} + +#webconsole-sidebar > tabs { + height: 0; + overflow: hidden; +}
--- a/dom/tests/browser/browser_ConsoleAPITests.js +++ b/dom/tests/browser/browser_ConsoleAPITests.js @@ -321,17 +321,18 @@ function testConsoleTimeEnd(aMessageObje ok(aMessageObject.arguments, "we have arguments"); is(aMessageObject.filename, gArgs[0].filename, "filename matches"); is(aMessageObject.lineNumber, gArgs[0].lineNumber, "lineNumber matches"); is(aMessageObject.functionName, gArgs[0].functionName, "functionName matches"); is(aMessageObject.arguments.length, gArgs[0].arguments.length, "arguments.length matches"); is(aMessageObject.timer.name, gArgs[0].timer.name, "timer name matches"); is(typeof aMessageObject.timer.duration, "number", "timer duration is a number"); - ok(aMessageObject.timer.duration > 0, "timer duration is positive"); + info("timer duration: " + aMessageObject.timer.duration); + ok(aMessageObject.timer.duration >= 0, "timer duration is positive"); gArgs[0].arguments.forEach(function (a, i) { is(aMessageObject.arguments[i], a, "correct arg " + i); }); startEmptyTimerTest(); }
new file mode 100644 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-unsafeDereference-01.js @@ -0,0 +1,10 @@ +// Debugger.Object.prototype.unsafeDereference returns the referent directly. + +var g = newGlobal(); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +assertEq(gw.getOwnPropertyDescriptor('Math').value.unsafeDereference(), g.Math); + +g.eval('var obj = {}'); +assertEq(gw.getOwnPropertyDescriptor('obj').value.unsafeDereference(), g.obj);
--- a/js/src/vm/Debugger.cpp +++ b/js/src/vm/Debugger.cpp @@ -4774,16 +4774,24 @@ DebuggerObject_unwrap(JSContext *cx, uns } args.rval().setObject(*unwrapped); if (!dbg->wrapDebuggeeValue(cx, args.rval())) return false; return true; } +static JSBool +DebuggerObject_unsafeDereference(JSContext *cx, unsigned argc, Value *vp) +{ + THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, "unsafeDereference", args, referent); + args.rval().setObject(*referent); + return cx->compartment->wrap(cx, args.rval()); +} + static JSPropertySpec DebuggerObject_properties[] = { JS_PSG("proto", DebuggerObject_getProto, 0), JS_PSG("class", DebuggerObject_getClass, 0), JS_PSG("callable", DebuggerObject_getCallable, 0), JS_PSG("name", DebuggerObject_getName, 0), JS_PSG("displayName", DebuggerObject_getDisplayName, 0), JS_PSG("parameterNames", DebuggerObject_getParameterNames, 0), JS_PSG("script", DebuggerObject_getScript, 0), @@ -4805,16 +4813,17 @@ static JSFunctionSpec DebuggerObject_met JS_FN("isFrozen", DebuggerObject_isFrozen, 0, 0), JS_FN("isExtensible", DebuggerObject_isExtensible, 0, 0), JS_FN("apply", DebuggerObject_apply, 0, 0), JS_FN("call", DebuggerObject_call, 0, 0), JS_FN("makeDebuggeeValue", DebuggerObject_makeDebuggeeValue, 1, 0), JS_FN("evalInGlobal", DebuggerObject_evalInGlobal, 1, 0), JS_FN("evalInGlobalWithBindings", DebuggerObject_evalInGlobalWithBindings, 2, 0), JS_FN("unwrap", DebuggerObject_unwrap, 0, 0), + JS_FN("unsafeDereference", DebuggerObject_unsafeDereference, 0, 0), JS_FS_END }; /*** Debugger.Environment ************************************************************************/ static void DebuggerEnv_trace(JSTracer *trc, RawObject obj)
--- a/toolkit/devtools/debugger/dbg-client.jsm +++ b/toolkit/devtools/debugger/dbg-client.jsm @@ -8,17 +8,18 @@ const Ci = Components.interfaces; const Cc = Components.classes; const Cu = Components.utils; const Cr = Components.results; this.EXPORTED_SYMBOLS = ["DebuggerTransport", "DebuggerClient", "debuggerSocketConnect", - "LongStringClient"]; + "LongStringClient", + "GripClient"]; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); const { defer, resolve, reject } = Promise; XPCOMUtils.defineLazyServiceGetter(this, "socketTransportService", @@ -168,17 +169,16 @@ const ThreadStateTypes = { /** * Set of protocol messages that are sent by the server without a prior request * by the client. */ const UnsolicitedNotifications = { "consoleAPICall": "consoleAPICall", "eventNotification": "eventNotification", "fileActivity": "fileActivity", - "locationChange": "locationChange", "networkEvent": "networkEvent", "networkEventUpdate": "networkEventUpdate", "newGlobal": "newGlobal", "newScript": "newScript", "newSource": "newSource", "tabDetached": "tabDetached", "tabNavigated": "tabNavigated", "pageError": "pageError", @@ -509,17 +509,17 @@ DebuggerClient.prototype = { this.notify(aPacket.type, aPacket); } if (onResponse) { onResponse(aPacket); } } catch(ex) { dumpn("Error handling response: " + ex + " - stack:\n" + ex.stack); - Cu.reportError(ex.message + "\n" + ex.stack); + Cu.reportError(ex + "\n" + ex.stack); } this._sendRequests(); }.bind(this)); }, /** * Called by DebuggerTransport when the underlying stream is closed.
--- 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. @@ -895,17 +910,17 @@ ThreadActor.prototype = { } if (aPool.objectActors.has(aValue)) { return aPool.objectActors.get(aValue).grip(); } else if (this.threadLifetimePool.objectActors.has(aValue)) { return this.threadLifetimePool.objectActors.get(aValue).grip(); } - let actor = new ObjectActor(aValue, this); + let actor = new PauseScopedObjectActor(aValue, this); aPool.addActor(actor); aPool.objectActors.set(aValue, actor); return actor.grip(); }, /** * Create a grip for the given debuggee object with a pause lifetime. * @@ -1510,20 +1525,17 @@ SourceActor.prototype.requestTypes = { * The parent thread actor for this object. */ function ObjectActor(aObj, aThreadActor) { this.obj = aObj; this.threadActor = aThreadActor; } -ObjectActor.prototype = Object.create(PauseScopedActor.prototype); - -update(ObjectActor.prototype, { - constructor: ObjectActor, +ObjectActor.prototype = { actorPrefix: "obj", /** * Returns a grip for this actor for returning in a protocol message. */ grip: function OA_grip() { let g = { "type": "object", "class": this.obj.class, @@ -1547,47 +1559,47 @@ update(ObjectActor.prototype, { return g; }, /** * Releases this actor from the pool. */ release: function OA_release() { - this.registeredPool.objectActors.delete(this.obj); + if (this.registeredPool.objectActors) { + this.registeredPool.objectActors.delete(this.obj); + } this.registeredPool.removeActor(this); this.disconnect(); }, disconnect: function OA_disconnect() { this.threadActor._removeFromProtoChain(this.obj); }, /** * Handle a protocol request to provide the names of the properties defined on * the object and not its prototype. * * @param aRequest object * The protocol request object. */ - onOwnPropertyNames: - PauseScopedActor.withPaused(function OA_onOwnPropertyNames(aRequest) { + onOwnPropertyNames: function OA_onOwnPropertyNames(aRequest) { return { from: this.actorID, ownPropertyNames: this.obj.getOwnPropertyNames() }; - }), + }, /** * Handle a protocol request to provide the prototype and own properties of * the object. * * @param aRequest object * The protocol request object. */ - onPrototypeAndProperties: - PauseScopedActor.withPaused(function OA_onPrototypeAndProperties(aRequest) { + onPrototypeAndProperties: function OA_onPrototypeAndProperties(aRequest) { if (this.obj.proto) { // Store the object and its prototype to the prototype chain cache, so that // we can evaluate native getter methods for WebIDL attributes that are // meant to be called on the instace and not on the prototype. // // TODO: after bug 801084, we could restrict the cache to objects where // this.obj.hostAnnotations.isWebIDLObject == true let chain = this.threadActor._findProtoChain(this.obj); @@ -1603,45 +1615,45 @@ update(ObjectActor.prototype, { let ownProperties = {}; for (let name of this.obj.getOwnPropertyNames()) { ownProperties[name] = this._propertyDescriptor(name); } return { from: this.actorID, prototype: this.threadActor.createValueGrip(this.obj.proto), ownProperties: ownProperties }; - }), + }, /** * Handle a protocol request to provide the prototype of the object. * * @param aRequest object * The protocol request object. */ - onPrototype: PauseScopedActor.withPaused(function OA_onPrototype(aRequest) { + onPrototype: function OA_onPrototype(aRequest) { return { from: this.actorID, prototype: this.threadActor.createValueGrip(this.obj.proto) }; - }), + }, /** * Handle a protocol request to provide the property descriptor of the * object's specified property. * * @param aRequest object * The protocol request object. */ - onProperty: PauseScopedActor.withPaused(function OA_onProperty(aRequest) { + onProperty: function OA_onProperty(aRequest) { if (!aRequest.name) { return { error: "missingParameter", message: "no property name was specified" }; } return { from: this.actorID, descriptor: this._propertyDescriptor(aRequest.name) }; - }), + }, /** * A helper method that creates a property descriptor for the provided object, * properly formatted for sending in a protocol response. * * @param string aName * The property that the descriptor is generated for. */ @@ -1719,26 +1731,94 @@ update(ObjectActor.prototype, { }, /** * Handle a protocol request to provide the source code of a function. * * @param aRequest object * The protocol request object. */ - onDecompile: PauseScopedActor.withPaused(function OA_onDecompile(aRequest) { + onDecompile: function OA_onDecompile(aRequest) { if (this.obj.class !== "Function") { return { error: "objectNotFunction", message: "decompile request is only valid for object grips " + "with a 'Function' class." }; } return { from: this.actorID, decompiledCode: this.obj.decompile(!!aRequest.pretty) }; - }), + }, + + /** + * Handle a protocol request to provide the parameters of a function. + * + * @param aRequest object + * The protocol request object. + */ + onParameterNames: function OA_onParameterNames(aRequest) { + if (this.obj.class !== "Function") { + return { error: "objectNotFunction", + message: "'parameterNames' request is only valid for object " + + "grips with a 'Function' class." }; + } + + return { parameterNames: this.obj.parameterNames }; + }, + + /** + * Handle a protocol request to release a thread-lifetime grip. + * + * @param aRequest object + * The protocol request object. + */ + onRelease: function OA_onRelease(aRequest) { + this.release(); + return {}; + }, +}; + +ObjectActor.prototype.requestTypes = { + "parameterNames": ObjectActor.prototype.onParameterNames, + "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties, + "prototype": ObjectActor.prototype.onPrototype, + "property": ObjectActor.prototype.onProperty, + "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames, + "decompile": ObjectActor.prototype.onDecompile, + "release": ObjectActor.prototype.onRelease, +}; + + +/** + * Creates a pause-scoped actor for the specified object. + * @see ObjectActor + */ +function PauseScopedObjectActor() +{ + ObjectActor.apply(this, arguments); +} + +PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype); + +update(PauseScopedObjectActor.prototype, ObjectActor.prototype); + +update(PauseScopedObjectActor.prototype, { + constructor: PauseScopedObjectActor, + + onOwnPropertyNames: + PauseScopedActor.withPaused(ObjectActor.prototype.onOwnPropertyNames), + + onPrototypeAndProperties: + PauseScopedActor.withPaused(ObjectActor.prototype.onPrototypeAndProperties), + + onPrototype: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototype), + onProperty: PauseScopedActor.withPaused(ObjectActor.prototype.onProperty), + onDecompile: PauseScopedActor.withPaused(ObjectActor.prototype.onDecompile), + + onParameterNames: + PauseScopedActor.withPaused(ObjectActor.prototype.onParameterNames), /** * Handle a protocol request to provide the lexical scope of a function. * * @param aRequest object * The protocol request object. */ onScope: PauseScopedActor.withPaused(function OA_onScope(aRequest) { @@ -1754,32 +1834,16 @@ update(ObjectActor.prototype, { return { error: "notDebuggee", message: "cannot access the environment of this function." }; } return { from: this.actorID, scope: envActor.form() }; }), /** - * Handle a protocol request to provide the parameters of a function. - * - * @param aRequest object - * The protocol request object. - */ - onParameterNames: PauseScopedActor.withPaused(function OA_onParameterNames(aRequest) { - if (this.obj.class !== "Function") { - return { error: "objectNotFunction", - message: "'parameterNames' request is only valid for object " + - "grips with a 'Function' class." }; - } - - return { parameterNames: this.obj.parameterNames }; - }), - - /** * Handle a protocol request to promote a pause-lifetime grip to a * thread-lifetime grip. * * @param aRequest object * The protocol request object. */ onThreadGrip: PauseScopedActor.withPaused(function OA_onThreadGrip(aRequest) { this.threadActor.threadObjectGrip(this); @@ -1798,27 +1862,20 @@ update(ObjectActor.prototype, { message: "Only thread-lifetime actors can be released." }; } this.release(); return {}; }), }); -ObjectActor.prototype.requestTypes = { - "parameterNames": ObjectActor.prototype.onParameterNames, - "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties, - "prototype": ObjectActor.prototype.onPrototype, - "property": ObjectActor.prototype.onProperty, - "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames, - "scope": ObjectActor.prototype.onScope, - "decompile": ObjectActor.prototype.onDecompile, - "threadGrip": ObjectActor.prototype.onThreadGrip, - "release": ObjectActor.prototype.onRelease, -}; +update(PauseScopedObjectActor.prototype.requestTypes, { + "scope": PauseScopedObjectActor.prototype.onScope, + "threadGrip": PauseScopedObjectActor.prototype.onThreadGrip, +}); /** * Creates an actor for the specied "very long" string. "Very long" is specified * at the server's discretion. * * @param aString String * The string.
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/debugger/tests/mochitest/Makefile.in @@ -0,0 +1,19 @@ +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = @relativesrcdir@ + +include $(DEPTH)/config/autoconf.mk + +MOCHITEST_CHROME_FILES = \ + test_unsafeDereference.html \ + nonchrome_unsafeDereference.html \ + $(NULL) + +include $(topsrcdir)/config/rules.mk
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/debugger/tests/mochitest/moz.build @@ -0,0 +1,5 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/debugger/tests/mochitest/nonchrome_unsafeDereference.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> +<html> +<script> +var xhr = new XMLHttpRequest; +xhr.timeout = 1742; +xhr.expando = 'Expando!'; +</script> +</html>
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/debugger/tests/mochitest/test_unsafeDereference.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=837723 + +When we use Debugger.Object.prototype.unsafeDereference to get a non-D.O +reference to a content object in chrome, that reference should be via an +xray wrapper. +--> +<head> + <meta charset="utf-8"> + <title>Mozilla Bug 837723</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> + +Components.utils.import("resource://gre/modules/jsdebugger.jsm"); +addDebuggerToGlobal(this); + +window.onload = function () { + SimpleTest.waitForExplicitFinish(); + + var iframe = document.createElement("iframe"); + iframe.src = "http://mochi.test:8888/chrome/toolkit/devtools/debugger/tests/mochitest/nonchrome_unsafeDereference.html"; + + iframe.onload = function () { + var dbg = new Debugger; + var contentDO = dbg.addDebuggee(iframe.contentWindow); + var xhrDesc = contentDO.getOwnPropertyDescriptor('xhr'); + + isnot(xhrDesc, undefined, "xhr should be visible as property of content global"); + isnot(xhrDesc.value, undefined, "xhr should have a value"); + + var xhr = xhrDesc.value.unsafeDereference(); + + is(typeof xhr, "object", "we should be able to deference xhr's value's D.O"); + is(xhr.timeout, 1742, "chrome should see the xhr's 'timeout' property"); + is(xhr.expando, undefined, "chrome should not see the xhr's 'expando' property"); + + SimpleTest.finish(); + } + + document.body.appendChild(iframe); +} + +</script> +</pre> +</body> +</html>
--- a/toolkit/devtools/debugger/tests/moz.build +++ b/toolkit/devtools/debugger/tests/moz.build @@ -1,8 +1,9 @@ # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +DIRS += ['mochitest'] + MODULE = 'test_debugger' -
--- 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); + } +}
new file mode 100644 --- /dev/null +++ b/toolkit/devtools/debugger/tests/unit/test_unsafeDereference.js @@ -0,0 +1,134 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// Test Debugger.Object.prototype.unsafeDereference in the presence of +// interesting cross-compartment wrappers. +// +// This is not really a debugger server test; it's more of a Debugger test. +// But we need xpcshell and Components.utils.Sandbox to get +// cross-compartment wrappers with interesting properties, and this is the +// xpcshell test directory most closely related to the JS Debugger API. + +Components.utils.import("resource://gre/modules/jsdebugger.jsm"); +addDebuggerToGlobal(this); + +// Add a method to Debugger.Object for fetching value properties +// conveniently. +Debugger.Object.prototype.getProperty = function (aName) { + let desc = this.getOwnPropertyDescriptor(aName); + if (!desc) + return undefined; + if (!desc.value) { + throw Error("Debugger.Object.prototype.getProperty: " + + "not a value property: " + aName); + } + return desc.value; +}; + +function run_test() { + // Create a low-privilege sandbox, and a chrome-privilege sandbox. + let contentBox = Components.utils.Sandbox('http://www.example.com'); + let chromeBox = Components.utils.Sandbox(this); + + // Create an objects in this compartment, and one in each sandbox. We'll + // refer to the objects as "mainObj", "contentObj", and "chromeObj", in + // variable and property names. + var mainObj = { name: "mainObj" }; + Components.utils.evalInSandbox('var contentObj = { name: "contentObj" };', + contentBox); + Components.utils.evalInSandbox('var chromeObj = { name: "chromeObj" };', + chromeBox); + + // Give each global a pointer to all the other globals' objects. + contentBox.mainObj = chromeBox.mainObj = mainObj; + var contentObj = chromeBox.contentObj = contentBox.contentObj; + var chromeObj = contentBox.chromeObj = chromeBox.chromeObj; + + // First, a whole bunch of basic sanity checks, to ensure that JavaScript + // evaluated in various scopes really does see the world the way this + // test expects it to. + + // The objects appear as global variables in the sandbox, and as + // the sandbox object's properties in chrome. + do_check_true(Components.utils.evalInSandbox('mainObj', contentBox) + === contentBox.mainObj); + do_check_true(Components.utils.evalInSandbox('contentObj', contentBox) + === contentBox.contentObj); + do_check_true(Components.utils.evalInSandbox('chromeObj', contentBox) + === contentBox.chromeObj); + do_check_true(Components.utils.evalInSandbox('mainObj', chromeBox) + === chromeBox.mainObj); + do_check_true(Components.utils.evalInSandbox('contentObj', chromeBox) + === chromeBox.contentObj); + do_check_true(Components.utils.evalInSandbox('chromeObj', chromeBox) + === chromeBox.chromeObj); + + // We (the main global) can see properties of all objects in all globals. + do_check_true(contentBox.mainObj.name === "mainObj"); + do_check_true(contentBox.contentObj.name === "contentObj"); + do_check_true(contentBox.chromeObj.name === "chromeObj"); + + // chromeBox can see properties of all objects in all globals. + do_check_eq(Components.utils.evalInSandbox('mainObj.name', chromeBox), + 'mainObj'); + do_check_eq(Components.utils.evalInSandbox('contentObj.name', chromeBox), + 'contentObj'); + do_check_eq(Components.utils.evalInSandbox('chromeObj.name', chromeBox), + 'chromeObj'); + + // contentBox can see properties of the content object, but not of either + // chrome object, because by default, content -> chrome wrappers hide all + // object properties. + do_check_eq(Components.utils.evalInSandbox('mainObj.name', contentBox), + undefined); + do_check_eq(Components.utils.evalInSandbox('contentObj.name', contentBox), + 'contentObj'); + do_check_eq(Components.utils.evalInSandbox('chromeObj.name', contentBox), + undefined); + + // When viewing an object in compartment A from the vantage point of + // compartment B, Debugger should give the same results as debuggee code + // would. + + // Create a debugger, debugging our two sandboxes. + let dbg = new Debugger; + + // Create Debugger.Object instances referring to the two sandboxes, as + // seen from their own compartments. + let contentBoxDO = dbg.addDebuggee(contentBox); + let chromeBoxDO = dbg.addDebuggee(chromeBox); + + // Use Debugger to view the objects from contentBox. We should get the + // same D.O instance from both getProperty and makeDebuggeeValue, and the + // same property visibility we checked for above. + let mainFromContentDO = contentBoxDO.getProperty('mainObj'); + do_check_eq(mainFromContentDO, contentBoxDO.makeDebuggeeValue(mainObj)); + do_check_eq(mainFromContentDO.getProperty('name'), undefined); + do_check_eq(mainFromContentDO.unsafeDereference(), mainObj); + + let contentFromContentDO = contentBoxDO.getProperty('contentObj'); + do_check_eq(contentFromContentDO, contentBoxDO.makeDebuggeeValue(contentObj)); + do_check_eq(contentFromContentDO.getProperty('name'), 'contentObj'); + do_check_eq(contentFromContentDO.unsafeDereference(), contentObj); + + let chromeFromContentDO = contentBoxDO.getProperty('chromeObj'); + do_check_eq(chromeFromContentDO, contentBoxDO.makeDebuggeeValue(chromeObj)); + do_check_eq(chromeFromContentDO.getProperty('name'), undefined); + do_check_eq(chromeFromContentDO.unsafeDereference(), chromeObj); + + // Similarly, viewing from chromeBox. + let mainFromChromeDO = chromeBoxDO.getProperty('mainObj'); + do_check_eq(mainFromChromeDO, chromeBoxDO.makeDebuggeeValue(mainObj)); + do_check_eq(mainFromChromeDO.getProperty('name'), 'mainObj'); + do_check_eq(mainFromChromeDO.unsafeDereference(), mainObj); + + let contentFromChromeDO = chromeBoxDO.getProperty('contentObj'); + do_check_eq(contentFromChromeDO, chromeBoxDO.makeDebuggeeValue(contentObj)); + do_check_eq(contentFromChromeDO.getProperty('name'), 'contentObj'); + do_check_eq(contentFromChromeDO.unsafeDereference(), contentObj); + + let chromeFromChromeDO = chromeBoxDO.getProperty('chromeObj'); + do_check_eq(chromeFromChromeDO, chromeBoxDO.makeDebuggeeValue(chromeObj)); + do_check_eq(chromeFromChromeDO.getProperty('name'), 'chromeObj'); + do_check_eq(chromeFromChromeDO.unsafeDereference(), chromeObj); +}
--- a/toolkit/devtools/debugger/tests/unit/xpcshell.ini +++ b/toolkit/devtools/debugger/tests/unit/xpcshell.ini @@ -104,8 +104,9 @@ reason = bug 820380 [test_longstringgrips-02.js] [test_source-01.js] skip-if = toolkit == "gonk" reason = bug 820380 [test_breakpointstore.js] [test_profiler_actor.js] skip-if = toolkit == "gonk" reason = bug 820380 +[test_unsafeDereference.js]
--- a/toolkit/devtools/webconsole/WebConsoleClient.jsm +++ b/toolkit/devtools/webconsole/WebConsoleClient.jsm @@ -76,23 +76,45 @@ WebConsoleClient.prototype = { /** * Evaluate a JavaScript expression. * * @param string aString * The code you want to evaluate. * @param function aOnResponse * The function invoked when the response is received. + * @param object [aOptions={}] + * Options for evaluation: + * + * - bindObjectActor: an ObjectActor ID. The OA holds a reference to + * a Debugger.Object that wraps a content object. This option allows + * you to bind |_self| to the D.O of the given OA, during string + * evaluation. + * + * See: Debugger.Object.evalInGlobalWithBindings() for information + * about bindings. + * + * Use case: the variable view needs to update objects and it does so + * by knowing the ObjectActor it inspects and binding |_self| to the + * D.O of the OA. As such, variable view sends strings like these for + * eval: + * _self["prop"] = value; + * + * - frameActor: a FrameActor ID. The FA holds a reference to + * a Debugger.Frame. This option allows you to evaluate the string in + * the frame of the given FA. */ - evaluateJS: function WCC_evaluateJS(aString, aOnResponse) + evaluateJS: function WCC_evaluateJS(aString, aOnResponse, aOptions = {}) { let packet = { to: this._actor, type: "evaluateJS", text: aString, + bindObjectActor: aOptions.bindObjectActor, + frameActor: aOptions.frameActor, }; this._client.request(packet, aOnResponse); }, /** * Autocomplete a JavaScript expression. * * @param string aString
--- a/toolkit/devtools/webconsole/WebConsoleUtils.jsm +++ b/toolkit/devtools/webconsole/WebConsoleUtils.jsm @@ -23,22 +23,27 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper", "resource://gre/modules/devtools/NetworkHelper.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "gActivityDistributor", "@mozilla.org/network/http-activity-distributor;1", "nsIHttpActivityDistributor"); +// TODO: Bug 842672 - toolkit/ imports modules from browser/. +// Note that these are only used in JSTermHelpers, see $0 and pprint(). XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TargetFactory", "resource:///modules/devtools/Target.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "VariablesView", + "resource:///modules/devtools/VariablesView.jsm"); + this.EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider", "JSTermHelpers", "PageErrorListener", "ConsoleAPIListener", "NetworkResponseListener", "NetworkMonitor", "ConsoleProgressListener"]; // Match the function name from the result of toString() or toSource(). // // Examples: @@ -781,17 +786,17 @@ this.WebConsoleUtils = { * Get the object class name. For example, the |window| object has the Window * class name (based on [object Window]). * * @param object aObject * The object you want to get the class name for. * @return string * The object class name. */ - getObjectClassName: function WCF_getObjectClassName(aObject) + getObjectClassName: function WCU_getObjectClassName(aObject) { if (aObject === null) { return "null"; } if (aObject === undefined) { return "undefined"; } @@ -854,16 +859,29 @@ this.WebConsoleUtils = { if (val.displayString && typeof val.displayString == "object" && val.displayString.type == "longString") { return val.displayString.initial; } return val.displayString || val.type; }, + + /** + * Check if the given value is a grip with an actor. + * + * @param mixed aGrip + * Value you want to check if it is a grip with an actor. + * @return boolean + * True if the given value is a grip with an actor. + */ + isActorGrip: function WCU_isActorGrip(aGrip) + { + return aGrip && typeof(aGrip) == "object" && aGrip.actor; + }, }; ////////////////////////////////////////////////////////////////////////// // Localization ////////////////////////////////////////////////////////////////////////// WebConsoleUtils.l10n = function WCU_l10n(aBundleURI) { @@ -1537,60 +1555,53 @@ this.JSTermHelpers = function JSTermHelp * @param string aXPath * xPath search query to execute. * @param [optional] nsIDOMNode aContext * Context to run the xPath query on. Uses window.document if not set. * @return array of nsIDOMNode */ aOwner.sandbox.$x = function JSTH_$x(aXPath, aContext) { - let nodes = []; + let nodes = new aOwner.window.wrappedJSObject.Array(); let doc = aOwner.window.document; let aContext = aContext || doc; - try { - let results = doc.evaluate(aXPath, aContext, null, - Ci.nsIDOMXPathResult.ANY_TYPE, null); - let node; - while (node = results.iterateNext()) { - nodes.push(node); - } - } - catch (ex) { - aOwner.window.console.error(ex.message); + let results = doc.evaluate(aXPath, aContext, null, + Ci.nsIDOMXPathResult.ANY_TYPE, null); + let node; + while (node = results.iterateNext()) { + nodes.push(node); } return nodes; }; /** * Returns the currently selected object in the highlighter. * * TODO: this implementation crosses the client/server boundaries! This is not * usable within a remote browser. To implement this feature correctly we need * support for remote inspection capabilities within the Inspector as well. * See bug 787975. * * @return nsIDOMElement|null * The DOM element currently selected in the highlighter. */ - Object.defineProperty(aOwner.sandbox, "$0", { + Object.defineProperty(aOwner.sandbox, "$0", { get: function() { - try { - let window = aOwner.chromeWindow(); - let target = TargetFactory.forTab(window.gBrowser.selectedTab); - let toolbox = gDevTools.getToolbox(target); + let window = aOwner.chromeWindow(); + if (!window) { + return null; + } + let target = TargetFactory.forTab(window.gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + let panel = toolbox ? toolbox.getPanel("inspector") : null; + let node = panel ? panel.selection.node : null; - return toolbox == null ? - undefined : - toolbox.getPanel("inspector").selection.node; - } - catch (ex) { - aOwner.window.console.error(ex.message); - } + return node ? aOwner.makeDebuggeeValue(node) : null; }, enumerable: true, configurable: false }); /** * Clears the output of the JSTerm. */ @@ -1605,38 +1616,33 @@ this.JSTermHelpers = function JSTermHelp * Returns the result of Object.keys(aObject). * * @param object aObject * Object to return the property names from. * @return array of strings */ aOwner.sandbox.keys = function JSTH_keys(aObject) { - return Object.keys(WebConsoleUtils.unwrap(aObject)); + return aOwner.window.wrappedJSObject.Object.keys(WebConsoleUtils.unwrap(aObject)); }; /** * Returns the values of all properties on aObject. * * @param object aObject * Object to display the values from. * @return array of string */ aOwner.sandbox.values = function JSTH_values(aObject) { - let arrValues = []; + let arrValues = new aOwner.window.wrappedJSObject.Array(); let obj = WebConsoleUtils.unwrap(aObject); - try { - for (let prop in obj) { - arrValues.push(obj[prop]); - } - } - catch (ex) { - aOwner.window.console.error(ex.message); + for (let prop in obj) { + arrValues.push(obj[prop]); } return arrValues; }; /** * Opens a help window in MDN. */ @@ -1648,25 +1654,22 @@ this.JSTermHelpers = function JSTermHelp /** * Inspects the passed aObject. This is done by opening the PropertyPanel. * * @param object aObject * Object to inspect. */ aOwner.sandbox.inspect = function JSTH_inspect(aObject) { - let obj = WebConsoleUtils.unwrap(aObject); - if (!WebConsoleUtils.isObjectInspectable(obj)) { - return aObject; - } - + let dbgObj = aOwner.makeDebuggeeValue(aObject); + let grip = aOwner.createValueGrip(dbgObj); aOwner.helperResult = { type: "inspectObject", input: aOwner.evalInput, - object: aOwner.createValueGrip(obj), + object: grip, }; }; /** * Prints aObject to the output. * * @param object aObject * Object to print to the output. @@ -1685,23 +1688,34 @@ this.JSTermHelpers = function JSTermHelp aOwner.helperResult = { rawOutput: true }; if (typeof aObject == "function") { return aObject + "\n"; } let output = []; - let getObjectGrip = WebConsoleUtils.getObjectGrip.bind(WebConsoleUtils); + let obj = WebConsoleUtils.unwrap(aObject); - let props = WebConsoleUtils.inspectObject(obj, getObjectGrip); - props.forEach(function(aProp) { - output.push(aProp.name + ": " + - WebConsoleUtils.getPropertyPanelValue(aProp)); - }); + for (let name in obj) { + let desc = WebConsoleUtils.getPropertyDescriptor(obj, name) || {}; + if (desc.get || desc.set) { + // TODO: Bug 842672 - toolkit/ imports modules from browser/. + let getGrip = VariablesView.getGrip(desc.get); + let setGrip = VariablesView.getGrip(desc.set); + let getString = VariablesView.getString(getGrip); + let setString = VariablesView.getString(setGrip); + output.push(name + ":", " get: " + getString, " set: " + setString); + } + else { + let valueGrip = VariablesView.getGrip(obj[name]); + let valueString = VariablesView.getString(valueGrip); + output.push(name + ": " + valueString); + } + } return " " + output.join("\n "); }; /** * Print a string to the output, as-is. * * @param string aString
--- a/toolkit/devtools/webconsole/dbg-webconsole-actors.js +++ b/toolkit/devtools/webconsole/dbg-webconsole-actors.js @@ -66,21 +66,33 @@ function WebConsoleActor(aConnection, aP this._window = Services.wm.getMostRecentWindow("navigator:browser"); this._isGlobalActor = true; } this._actorPool = new ActorPool(this.conn); this.conn.addActorPool(this._actorPool); this._prefs = {}; + + this.dbg = new Debugger(); + this._createGlobal(); + + this._protoChains = new Map(); } WebConsoleActor.prototype = { /** + * Debugger instance. + * + * @see jsdebugger.jsm + */ + dbg: null, + + /** * Tells if this Web Console actor is a global actor or not. * @private * @type boolean */ _isGlobalActor: false, /** * Actor pool for all of the actors we send to the client. @@ -93,28 +105,49 @@ WebConsoleActor.prototype = /** * Web Console-related preferences. * @private * @type object */ _prefs: null, /** - * Tells the current inner window associated to the sandbox. When the page - * is navigated, we recreate the sandbox. + * Tells the current inner window of the window of |this._dbgWindow|. When the + * page is navigated, we recreate the debugger object. + * @private + * @type object + */ + _globalWindowId: 0, + + /** + * The Debugger.Object that wraps the content window. * @private * @type object */ - _sandboxWindowId: 0, + _dbgWindow: null, /** - * The JavaScript Sandbox where code is evaluated. + * Object that holds the API we give to the JSTermHelpers constructor. This is + * where the JSTerm helper functions are added. + * + * @see this._getJSTermHelpers() + * @private * @type object */ - sandbox: null, + _jstermHelpers: null, + + /** + * A cache of prototype chains for objects that have received a + * prototypeAndProperties request. + * + * @private + * @type Map + * @see dbg-script-actors.js, ThreadActor._protoChains + */ + _protoChains: null, /** * The debugger server connection instance. * @type object */ conn: null, /** @@ -157,16 +190,21 @@ WebConsoleActor.prototype = grip: function WCA_grip() { return { actor: this.actorID }; }, hasNativeConsoleAPI: BrowserTabActor.prototype.hasNativeConsoleAPI, + _createValueGrip: ThreadActor.prototype.createValueGrip, + _stringIsLong: ThreadActor.prototype._stringIsLong, + _findProtoChain: ThreadActor.prototype._findProtoChain, + _removeFromProtoChain: ThreadActor.prototype._removeFromProtoChain, + /** * Destroy the current WebConsoleActor instance. */ disconnect: function WCA_disconnect() { if (this.pageErrorListener) { this.pageErrorListener.destroy(); this.pageErrorListener = null; @@ -180,74 +218,80 @@ WebConsoleActor.prototype = this.networkMonitor = null; } if (this.consoleProgressListener) { this.consoleProgressListener.destroy(); this.consoleProgressListener = null; } this.conn.removeActorPool(this._actorPool); this._actorPool = null; - this.sandbox = null; - this._sandboxWindowId = 0; + this._protoChains.clear(); + this.dbg.enabled = false; + this.dbg = null; + this._dbgWindow = null; + this._globalWindowId = 0; this.conn = this._window = null; }, /** - * Create a grip for the given value. If the value is an object, - * a WebConsoleObjectActor will be created. + * Create a grip for the given value. * * @param mixed aValue * @return object */ createValueGrip: function WCA_createValueGrip(aValue) { - return WebConsoleUtils.createValueGrip(aValue, - this.createObjectActor.bind(this)); + return this._createValueGrip(aValue, this._actorPool); + }, + + /** + * Make a debuggee value for the given value. + * + * @param mixed aValue + * The value you want to get a debuggee value for. + * @return object + * Debuggee value for |aValue|. + */ + makeDebuggeeValue: function WCA_makeDebuggeeValue(aValue) + { + return this._dbgWindow.makeDebuggeeValue(aValue); }, /** * Create a grip for the given object. * * @param object aObject * The object you want. + * @param object aPool + * An ActorPool where the new actor instance is added. * @param object * The object grip. */ - createObjectActor: function WCA_createObjectActor(aObject) + objectGrip: function WCA_objectGrip(aObject, aPool) { - if (typeof aObject == "string") { - return this.createStringGrip(aObject); - } - - // We need to unwrap the object, otherwise we cannot access the properties - // and methods added by the content scripts. - let obj = WebConsoleUtils.unwrap(aObject); - let actor = new WebConsoleObjectActor(obj, this); - this._actorPool.addActor(actor); + let actor = new ObjectActor(aObject, this); + aPool.addActor(actor); return actor.grip(); }, /** - * Create a grip for the given string. If the given string is a long string, - * then a LongStringActor grip will be used. + * Create a grip for the given string. * * @param string aString * The string you want to create the grip for. - * @return string|object - * The same string, as is, or a LongStringActor object that wraps the - * given string. + * @param object aPool + * An ActorPool where the new actor instance is added. + * @return object + * A LongStringActor object that wraps the given string. */ - createStringGrip: function WCA_createStringGrip(aString) + longStringGrip: function WCA_longStringGrip(aString, aPool) { - if (aString.length >= DebuggerServer.LONG_STRING_LENGTH) { - let actor = new LongStringActor(aString, this); - this._actorPool.addActor(actor); - return actor.grip(); - } - return aString; + let actor = new LongStringActor(aString, this); + aPool.addActor(actor); + return actor.grip(); }, /** * Get an object actor by its ID. * * @param string aActorID * @return object */ @@ -447,54 +491,68 @@ WebConsoleActor.prototype = * @param object aRequest * The JSON request object received from the Web Console client. * @return object * The evaluation response packet. */ onEvaluateJS: function WCA_onEvaluateJS(aRequest) { let input = aRequest.text; - let result, error = null; - let timestamp; + let timestamp = Date.now(); + + let evalOptions = { + bindObjectActor: aRequest.bindObjectActor, + frameActor: aRequest.frameActor, + }; + let evalInfo = this.evalWithDebugger(input, evalOptions); + let evalResult = evalInfo.result; + let helperResult = this._jstermHelpers.helperResult; + delete this._jstermHelpers.helperResult; - this.helperResult = null; - this.evalInput = input; - try { - timestamp = Date.now(); - result = this.evalInSandbox(input); + let result, error, errorMessage; + if (evalResult) { + if ("return" in evalResult) { + result = evalResult.return; + } + else if ("yield" in evalResult) { + result = evalResult.yield; + } + else if ("throw" in evalResult) { + error = evalResult.throw; + let errorToString = evalInfo.window + .evalInGlobalWithBindings("ex + ''", {ex: error}); + if (errorToString && typeof errorToString.return == "string") { + errorMessage = errorToString.return; + } + } } - catch (ex) { - error = ex; - } - - let helperResult = this.helperResult; - delete this.helperResult; - delete this.evalInput; return { from: this.actorID, input: input, result: this.createValueGrip(result), timestamp: timestamp, - error: error, - errorMessage: error ? String(error) : null, + exception: error ? this.createValueGrip(error) : null, + exceptionMessage: errorMessage, helperResult: helperResult, }; }, /** * The Autocomplete request handler. * * @param object aRequest * The request message - what input to autocomplete. * @return object * The response message - matched properties. */ onAutocomplete: function WCA_onAutocomplete(aRequest) { + // TODO: Bug 842682 - use the debugger API for autocomplete in the Web + // Console, and provide suggestions from the selected debugger stack frame. let result = JSPropertyProvider(this.window, aRequest.text) || {}; return { from: this.actorID, matches: result.matches || [], matchProp: result.matchProp, }; }, @@ -524,78 +582,211 @@ WebConsoleActor.prototype = return { updated: Object.keys(aRequest.preferences) }; }, ////////////////// // End of request handlers. ////////////////// /** - * Create the JavaScript sandbox where user input is evaluated. + * Create the Debugger.Object for the current window. * @private */ - _createSandbox: function WCA__createSandbox() + _createGlobal: function WCA__createGlobal() { - this._sandboxWindowId = WebConsoleUtils.getInnerWindowId(this.window); - this.sandbox = new Cu.Sandbox(this.window, { - sandboxPrototype: this.window, - wantXrays: false, - }); + let windowId = WebConsoleUtils.getInnerWindowId(this.window); + if (this._globalWindowId == windowId) { + return; + } + + this._globalWindowId = windowId; + + this._dbgWindow = this.dbg.addDebuggee(this.window); + this.dbg.removeDebuggee(this.window); + + // Update the JSTerm helpers. + this._jstermHelpers = this._getJSTermHelpers(this._dbgWindow); + }, - this.sandbox.console = this.window.console; + /** + * Create an object with the API we expose to the JSTermHelpers constructor. + * This object inherits properties and methods from the Web Console actor. + * + * @private + * @param object aDebuggerObject + * A Debugger.Object that wraps a content global. This is used for the + * JSTerm helpers. + * @return object + */ + _getJSTermHelpers: function WCA__getJSTermHelpers(aDebuggerObject) + { + let helpers = Object.create(this); + helpers.sandbox = Object.create(null); + helpers._dbgWindow = aDebuggerObject; + JSTermHelpers(helpers); - JSTermHelpers(this); + // Make sure the helpers can be used during eval. + for (let name in helpers.sandbox) { + let desc = Object.getOwnPropertyDescriptor(helpers.sandbox, name); + if (desc.get || desc.set) { + continue; + } + helpers.sandbox[name] = helpers.makeDebuggeeValue(desc.value); + } + return helpers; }, /** - * Evaluates a string in the sandbox. + * Evaluates a string using the debugger API. + * + * To allow the variables view to update properties from the web console we + * provide the "bindObjectActor" mechanism: the Web Console tells the + * ObjectActor ID for which it desires to evaluate an expression. The + * Debugger.Object pointed at by the actor ID is bound such that it is + * available during expression evaluation (evalInGlobalWithBindings()). + * + * Example: + * _self['foobar'] = 'test' + * where |_self| refers to the desired object. + * + * The |frameActor| property allows the Web Console client to provide the + * frame actor ID, such that the expression can be evaluated in the + * user-selected stack frame. + * + * For the above to work we need the debugger and the web console to share + * a connection, otherwise the Web Console actor will not find the frame + * actor. + * + * The Debugger.Frame comes from the jsdebugger's Debugger instance, which + * is different from the Web Console's Debugger instance. This means that + * for evaluation to work, we need to create a new instance for the jsterm + * helpers - they need to be Debugger.Objects coming from the jsdebugger's + * Debugger instance. * * @param string aString - * String to evaluate in the sandbox. - * @return mixed - * The result of the evaluation. + * String to evaluate. + * @param object [aOptions] + * Options for evaluation: + * - bindObjectActor: the ObjectActor ID to use for evaluation. + * |evalWithBindings()| will be called with one additional binding: + * |_self| which will point to the Debugger.Object of the given + * ObjectActor. + * - frameActor: the FrameActor ID to use for evaluation. The given + * debugger frame is used for evaluation, instead of the global window. + * @return object + * An object that holds the following properties: + * - dbg: the debugger where the string was evaluated. + * - frame: (optional) the frame where the string was evaluated. + * - window: the Debugger.Object for the global where the string was + * evaluated. + * - result: the result of the evaluation. */ - evalInSandbox: function WCA_evalInSandbox(aString) + evalWithDebugger: function WCA_evalWithDebugger(aString, aOptions = {}) { - // If the user changed to a different location, we need to update the - // sandbox. - if (this._sandboxWindowId !== WebConsoleUtils.getInnerWindowId(this.window)) { - this._createSandbox(); - } + this._createGlobal(); - // The help function needs to be easy to guess, so we make the () optional + // The help function needs to be easy to guess, so we make the () optional. if (aString.trim() == "help" || aString.trim() == "?") { aString = "help()"; } - let window = WebConsoleUtils.unwrap(this.sandbox.window); - let $ = null, $$ = null; + let bindSelf = null; + + if (aOptions.bindObjectActor) { + let objActor = this.getActorByID(aOptions.bindObjectActor); + if (objActor) { + bindSelf = objActor.obj; + } + } - // We prefer to execute the page-provided implementations for the $() and - // $$() functions. - if (typeof window.$ == "function") { - $ = this.sandbox.$; - delete this.sandbox.$; + let helpers = this._jstermHelpers; + let found$ = false, found$$ = false; + let frame = null, frameActor = null; + if (aOptions.frameActor) { + frameActor = this.conn.getActor(aOptions.frameActor); + if (frameActor) { + frame = frameActor.frame; + } + else { + Cu.reportError("Web Console Actor: the frame actor was not found: " + + aOptions.frameActor); + } } - if (typeof window.$$ == "function") { - $$ = this.sandbox.$$; - delete this.sandbox.$$; + + let dbg = this.dbg; + let dbgWindow = this._dbgWindow; + + if (frame) { + // Avoid having bindings from a different Debugger. The Debugger.Frame + // comes from the jsdebugger's Debugger instance. + dbg = frameActor.threadActor.dbg; + dbgWindow = dbg.addDebuggee(this.window); + helpers = this._getJSTermHelpers(dbgWindow); + + let env = frame.environment; + if (env) { + found$ = !!env.find("$"); + found$$ = !!env.find("$$"); + } + } + else { + found$ = !!this._dbgWindow.getOwnPropertyDescriptor("$"); + found$$ = !!this._dbgWindow.getOwnPropertyDescriptor("$$"); } - let result = Cu.evalInSandbox(aString, this.sandbox, "1.8", - "Web Console", 1); + let bindings = helpers.sandbox; + if (bindSelf) { + let jsObj = bindSelf.unsafeDereference(); + bindings._self = helpers.makeDebuggeeValue(jsObj); + } + + let $ = null, $$ = null; + if (found$) { + $ = bindings.$; + delete bindings.$; + } + if (found$$) { + $$ = bindings.$$; + delete bindings.$$; + } + + helpers.helperResult = null; + helpers.evalInput = aString; + + let result; + if (frame) { + result = frame.evalWithBindings(aString, bindings); + } + else { + result = this._dbgWindow.evalInGlobalWithBindings(aString, bindings); + } + + delete helpers.evalInput; + if (helpers != this._jstermHelpers) { + this._jstermHelpers.helperResult = helpers.helperResult; + delete helpers.helperResult; + } if ($) { - this.sandbox.$ = $; + bindings.$ = $; } if ($$) { - this.sandbox.$$ = $$; + bindings.$$ = $$; + } + + if (bindings._self) { + delete bindings._self; } - return result; + return { + result: result, + dbg: dbg, + frame: frame, + window: dbgWindow, + }; }, ////////////////// // Event handlers for various listeners. ////////////////// /** * Handler for page errors received from the PageErrorListener. This method @@ -721,130 +912,58 @@ WebConsoleActor.prototype = prepareConsoleMessageForRemote: function WCA_prepareConsoleMessageForRemote(aMessage) { let result = WebConsoleUtils.cloneObject(aMessage); delete result.wrappedJSObject; result.arguments = Array.map(aMessage.arguments || [], function(aObj) { - return this.createValueGrip(aObj); + let dbgObj = this.makeDebuggeeValue(aObj); + return this.createValueGrip(dbgObj); }, this); - if (result.level == "dir") { - result.objectProperties = []; - let first = result.arguments[0]; - if (typeof first == "object" && first && first.inspectable) { - let actor = this.getActorByID(first.actor); - result.objectProperties = actor.onInspectProperties().properties; - } - } - return result; }, /** * Find the XUL window that owns the content window. * * @return Window * The XUL window that owns the content window. */ chromeWindow: function WCA_chromeWindow() { - return this.window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell) - .chromeEventHandler.ownerDocument.defaultView; + let window = null; + try { + window = this.window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.ownerDocument.defaultView; + } + catch (ex) { + // The above can fail because chromeEventHandler is not available for all + // kinds of |this.window|. + } + + return window; }, }; WebConsoleActor.prototype.requestTypes = { startListeners: WebConsoleActor.prototype.onStartListeners, stopListeners: WebConsoleActor.prototype.onStopListeners, getCachedMessages: WebConsoleActor.prototype.onGetCachedMessages, evaluateJS: WebConsoleActor.prototype.onEvaluateJS, autocomplete: WebConsoleActor.prototype.onAutocomplete, clearMessagesCache: WebConsoleActor.prototype.onClearMessagesCache, setPreferences: WebConsoleActor.prototype.onSetPreferences, }; /** - * Creates an actor for the specified object. - * - * @constructor - * @param object aObj - * The object you want. - * @param object aWebConsoleActor - * The parent WebConsoleActor instance for this object. - */ -function WebConsoleObjectActor(aObj, aWebConsoleActor) -{ - this.obj = aObj; - this.parent = aWebConsoleActor; -} - -WebConsoleObjectActor.prototype = -{ - actorPrefix: "consoleObj", - - /** - * Returns a grip for this actor for returning in a protocol message. - */ - grip: function WCOA_grip() - { - let grip = WebConsoleUtils.getObjectGrip(this.obj); - grip.actor = this.actorID; - grip.displayString = this.parent.createStringGrip(grip.displayString); - return grip; - }, - - /** - * Releases this actor from the pool. - */ - release: function WCOA_release() - { - this.parent.releaseActor(this); - this.parent = this.obj = null; - }, - - /** - * Handle a protocol request to inspect the properties of the object. - * - * @return object - * Message to send to the client. This holds the 'properties' property - * - an array with a descriptor for each property in the object. - */ - onInspectProperties: function WCOA_onInspectProperties() - { - let createObjectActor = this.parent.createObjectActor.bind(this.parent); - let props = WebConsoleUtils.inspectObject(this.obj, createObjectActor); - return { - from: this.actorID, - properties: props, - }; - }, - - /** - * Handle a protocol request to release a grip. - */ - onRelease: function WCOA_onRelease() - { - this.release(); - return {}; - }, -}; - -WebConsoleObjectActor.prototype.requestTypes = -{ - "inspectProperties": WebConsoleObjectActor.prototype.onInspectProperties, - "release": WebConsoleObjectActor.prototype.onRelease, -}; - - -/** * Creates an actor for a network event. * * @constructor * @param object aNetworkEvent * The network event you want to use the actor for. * @param object aWebConsoleActor * The parent WebConsoleActor instance for this object. */ @@ -1078,17 +1197,17 @@ NetworkEventActor.prototype = * Add network request POST data. * * @param object aPostData * The request POST data. */ addRequestPostData: function NEA_addRequestPostData(aPostData) { this._request.postData = aPostData; - aPostData.text = this.parent.createStringGrip(aPostData.text); + aPostData.text = this._createStringGrip(aPostData.text); if (typeof aPostData.text == "object") { this._longStringActors.add(aPostData.text); } let packet = { from: this.actorID, type: "networkEventUpdate", updateType: "requestPostData", @@ -1173,17 +1292,17 @@ NetworkEventActor.prototype = * The response content. * @param boolean aDiscardedResponseBody * Tells if the response content was recorded or not. */ addResponseContent: function NEA_addResponseContent(aContent, aDiscardedResponseBody) { this._response.content = aContent; - aContent.text = this.parent.createStringGrip(aContent.text); + aContent.text = this._createStringGrip(aContent.text); if (typeof aContent.text == "object") { this._longStringActors.add(aContent.text); } let packet = { from: this.actorID, type: "networkEventUpdate", updateType: "responseContent", @@ -1223,22 +1342,40 @@ NetworkEventActor.prototype = * LongStringActor for the header values, when needed. * * @private * @param array aHeaders */ _prepareHeaders: function NEA__prepareHeaders(aHeaders) { for (let header of aHeaders) { - header.value = this.parent.createStringGrip(header.value); + header.value = this._createStringGrip(header.value); if (typeof header.value == "object") { this._longStringActors.add(header.value); } } }, + + /** + * Create a long string grip if needed for the given string. + * + * @private + * @param string aString + * The string you want to create a long string grip for. + * @return string|object + * A string is returned if |aString| is not a long string. + * A LongStringActor grip is returned if |aString| is a long string. + */ + _createStringGrip: function NEA__createStringGrip(aString) + { + if (this.parent._stringIsLong(aString)) { + return this.parent.longStringGrip(aString, this.parent._actorPool); + } + return aString; + }, }; NetworkEventActor.prototype.requestTypes = { "release": NetworkEventActor.prototype.onRelease, "getRequestHeaders": NetworkEventActor.prototype.onGetRequestHeaders, "getRequestCookies": NetworkEventActor.prototype.onGetRequestCookies, "getRequestPostData": NetworkEventActor.prototype.onGetRequestPostData,
--- a/toolkit/devtools/webconsole/test/test_bug819670_getter_throws.html +++ b/toolkit/devtools/webconsole/test/test_bug819670_getter_throws.html @@ -29,43 +29,43 @@ function onAttach(aState, aResponse) function onEvaluate(aState, aResponse) { checkObject(aResponse, { from: aState.actor, input: "document.__proto__", result: { type: "object", actor: /[a-z]/, - inspectable: true, }, }); - ok(!aResponse.error, "no js error"); + ok(!aResponse.exception, "no eval exception"); ok(!aResponse.helperResult, "no helper result"); onInspect = onInspect.bind(null, aState); - aState.client.inspectObjectProperties(aResponse.result.actor, onInspect); + let client = new GripClient(aState.dbgClient, aResponse.result); + client.getPrototypeAndProperties(onInspect); } function onInspect(aState, aResponse) { ok(!aResponse.error, "no response error"); - let expectedProps = [ - { name: "ATTRIBUTE_NODE", value: 2 }, - { name: "CDATA_SECTION_NODE", value: 4 }, - { name: "COMMENT_NODE", value: 8 }, - { name: "DOCUMENT_FRAGMENT_NODE", value: 11 }, - ]; + let expectedProps = { + "addBroadcastListenerFor": { value: { type: "object" } }, + "commandDispatcher": { get: { type: "object" } }, + "getBoxObjectFor": { value: { type: "object" } }, + "getElementsByAttribute": { value: { type: "object" } }, + }; - let props = aResponse.properties; + let props = aResponse.ownProperties; ok(props, "response properties available"); if (props) { - ok(props.length > expectedProps.length, + ok(Object.keys(props).length > Object.keys(expectedProps).length, "number of enumerable properties"); checkObject(props, expectedProps); } closeDebugger(aState, function() { SimpleTest.finish(); }); }
--- a/toolkit/devtools/webconsole/test/test_consoleapi.html +++ b/toolkit/devtools/webconsole/test/test_consoleapi.html @@ -15,23 +15,23 @@ SimpleTest.waitForExplicitFinish(); let expectedConsoleCalls = []; function doConsoleCalls(aState) { let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 2)).join("a"); - console.log("foobarBaz-log", undefined); - console.info("foobarBaz-info", null); - console.warn("foobarBaz-warn", document.body); - console.debug(null); - console.trace(); - console.dir(document, window); - console.log("foo", longString); + top.console.log("foobarBaz-log", undefined); + top.console.info("foobarBaz-info", null); + top.console.warn("foobarBaz-warn", top.document.documentElement); + top.console.debug(null); + top.console.trace(); + top.console.dir(top.document, top.location); + top.console.log("foo", longString); expectedConsoleCalls = [ { level: "log", filename: /test_consoleapi/, functionName: "doConsoleCalls", timeStamp: /^\d+$/, arguments: ["foobarBaz-log", { type: "undefined" }], @@ -77,34 +77,24 @@ function doConsoleCalls(aState) level: "dir", filename: /test_consoleapi/, functionName: "doConsoleCalls", timeStamp: /^\d+$/, arguments: [ { type: "object", actor: /[a-z]/, - className: "HTMLDocument", + class: "XULDocument", }, { type: "object", actor: /[a-z]/, - className: "Window", + class: "Location", } ], - objectProperties: [ - { - name: "ATTRIBUTE_NODE", - value: 2, - }, - { - name: "CDATA_SECTION_NODE", - value: 4, - }, // ... - ], }, { level: "log", filename: /test_consoleapi/, functionName: "doConsoleCalls", timeStamp: /^\d+$/, arguments: [ "foo", @@ -119,17 +109,17 @@ function doConsoleCalls(aState) }, ]; } function startTest() { removeEventListener("load", startTest); - attachConsole(["ConsoleAPI"], onAttach); + attachConsole(["ConsoleAPI"], onAttach, true); } function onAttach(aState, aResponse) { onConsoleAPICall = onConsoleAPICall.bind(null, aState); aState.dbgClient.addListener("consoleAPICall", onConsoleAPICall); doConsoleCalls(aState.actor); }
--- a/toolkit/devtools/webconsole/test/test_jsterm.html +++ b/toolkit/devtools/webconsole/test/test_jsterm.html @@ -86,41 +86,41 @@ function doSimpleEval() function onSimpleEval(aResponse) { checkObject(aResponse, { from: gState.actor, input: "2+2", result: 4, }); - ok(!aResponse.error, "no js error"); + ok(!aResponse.exception, "no eval exception"); ok(!aResponse.helperResult, "no helper result"); nextTest(); } function doWindowEval() { - info("test eval 'window'"); - gState.client.evaluateJS("window", onWindowEval); + info("test eval 'document'"); + gState.client.evaluateJS("document", onWindowEval); } function onWindowEval(aResponse) { checkObject(aResponse, { from: gState.actor, - input: "window", + input: "document", result: { type: "object", - className: "Window", + class: "XULDocument", actor: /[a-z]/, }, }); - ok(!aResponse.error, "no js error"); + ok(!aResponse.exception, "no eval exception"); ok(!aResponse.helperResult, "no helper result"); nextTest(); } function doEvalWithException() { info("test eval with exception"); @@ -130,20 +130,20 @@ function doEvalWithException() function onEvalWithException(aResponse) { checkObject(aResponse, { from: gState.actor, input: "window.doTheImpossible()", result: { type: "undefined", }, - errorMessage: /doTheImpossible/, + exceptionMessage: /doTheImpossible/, }); - ok(aResponse.error, "js error object"); + ok(aResponse.exception, "js eval exception"); ok(!aResponse.helperResult, "no helper result"); nextTest(); } function doEvalWithHelper() { info("test eval with helper"); @@ -156,17 +156,17 @@ function onEvalWithHelper(aResponse) from: gState.actor, input: "clear()", result: { type: "undefined", }, helperResult: { type: "clearOutput" }, }); - ok(!aResponse.error, "no js error"); + ok(!aResponse.exception, "no eval exception"); nextTest(); } function doEvalString() { gState.client.evaluateJS("window.foobarObject.strfoo", onEvalString); }
--- a/toolkit/devtools/webconsole/test/test_object_actor.html +++ b/toolkit/devtools/webconsole/test/test_object_actor.html @@ -25,174 +25,148 @@ function startTest() function onAttach(aState, aResponse) { onConsoleCall = onConsoleCall.bind(null, aState); aState.dbgClient.addListener("consoleAPICall", onConsoleCall); let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 3)).join("\u0629"); - window.foobarObject = Object.create(null); + // Here we put the objects in the correct window, to avoid having them all + // wrapped by proxies for cross-compartment access. + + let foobarObject = top.Object.create(null); foobarObject.tamarbuta = longString; foobarObject.foo = 1; foobarObject.foobar = "hello"; - foobarObject.foobaz = document; foobarObject.omg = null; foobarObject.testfoo = false; - foobarObject.notInspectable = {}; - foobarObject.omgfn = function _omgfn() { - return "myResult"; - }; - foobarObject.abArray = ["a", "b"]; + foobarObject.notInspectable = top.Object.create(null); + foobarObject.omgfn = new top.Function("return 'myResult'"); + foobarObject.abArray = new top.Array("a", "b"); + foobarObject.foobaz = top.document; - Object.defineProperty(foobarObject, "getterAndSetter", { + top.Object.defineProperty(foobarObject, "getterAndSetter", { enumerable: true, - get: function fooGet() { return "foo"; }, - set: function fooSet() { 1+2 }, + get: new top.Function("return 'foo';"), + set: new top.Function("1+2"), }); - foobarObject.longStringObj = { - toSource: function() longString, - toString: function() longString, - boom: "explode", - }; + foobarObject.longStringObj = top.Object.create(null); + foobarObject.longStringObj.toSource = new top.Function("'" + longString + "'"); + foobarObject.longStringObj.toString = new top.Function("'" + longString + "'"); + foobarObject.longStringObj.boom = "explode"; - console.log("hello", foobarObject); + top.wrappedJSObject.foobarObject = foobarObject; + top.console.log("hello", top.wrappedJSObject.foobarObject); - expectedProps = [ - { - name: "abArray", + expectedProps = { + "abArray": { value: { type: "object", - className: "Array", + class: "Array", actor: /[a-z]/, - inspectable: true, }, }, - { - name: "foo", + "foo": { configurable: true, enumerable: true, writable: true, value: 1, }, - { - name: "foobar", + "foobar": { value: "hello", }, - { - name: "foobaz", + "foobaz": { value: { type: "object", - className: "HTMLDocument", - displayString: /\[object HTMLDocument/, - inspectable: true, + class: "XULDocument", + actor: /[a-z]/, + }, + }, + "getterAndSetter": { + get: { + type: "object", + class: "Function", + actor: /[a-z]/, + }, + set: { + type: "object", + class: "Function", actor: /[a-z]/, }, }, - { - name: "getterAndSetter", - get: { - type: "function", - className: "Function", - displayString: /function fooGet/, + "longStringObj": { + value: { + type: "object", + class: "Object", actor: /[a-z]/, - inspectable: false, - }, - set: { - type: "function", - className: "Function", - displayString: /function fooSet/, - actor: /[a-z]/, - inspectable: false, }, }, - { - name: "longStringObj", + "notInspectable": { value: { type: "object", - className: "Object", + class: "Object", actor: /[a-z]/, - inspectable: true, - displayString: { - type: "longString", - initial: longString.substring(0, - DebuggerServer.LONG_STRING_INITIAL_LENGTH), - length: longString.length, - }, }, }, - { - name: "notInspectable", + "omg": { + value: { type: "null" }, + }, + "omgfn": { value: { type: "object", - className: "Object", + class: "Function", actor: /[a-z]/, - inspectable: false, }, }, - { - name: "omg", - value: { type: "null" }, - }, - { - name: "omgfn", - value: { - type: "function", - className: "Function", - displayString: /function _omgfn/, - actor: /[a-z]/, - inspectable: false, - }, - }, - { - name: "tamarbuta", + "tamarbuta": { value: { type: "longString", initial: longString.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH), length: longString.length, }, }, - { - name: "testfoo", + "testfoo": { value: false, }, - ]; + }; } function onConsoleCall(aState, aType, aPacket) { is(aPacket.from, aState.actor, "console API call actor"); info("checking the console API call packet"); checkConsoleAPICall(aPacket.message, { level: "log", filename: /test_object_actor/, functionName: "onAttach", arguments: ["hello", { type: "object", actor: /[a-z]/, - inspectable: true, }], }); aState.dbgClient.removeListener("consoleAPICall", onConsoleCall); info("inspecting object properties"); let args = aPacket.message.arguments; onProperties = onProperties.bind(null, aState); - aState.client.inspectObjectProperties(args[1].actor, onProperties); + + let client = new GripClient(aState.dbgClient, args[1]); + client.getPrototypeAndProperties(onProperties); } function onProperties(aState, aResponse) { - let props = aResponse.properties; - is(props.length, expectedProps.length, + let props = aResponse.ownProperties; + is(Object.keys(props).length, Object.keys(expectedProps).length, "number of enumerable properties"); checkObject(props, expectedProps); expectedProps = []; closeDebugger(aState, function() { SimpleTest.finish(); });
--- a/xpfe/components/autocomplete/src/Makefile.in +++ b/xpfe/components/autocomplete/src/Makefile.in @@ -7,16 +7,15 @@ DEPTH = @DEPTH@ topsrcdir = @top_srcdir@ srcdir = @srcdir@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk MODULE_NAME = xpAutoComplete LIBRARY_NAME = xpautocomplete -SHORT_LIBNAME = xpautoc EXPORT_LIBRARY = 1 LIBXUL_LIBRARY = 1 IS_COMPONENT = 1 CPPSRCS = nsAutoComplete.cpp include $(topsrcdir)/config/rules.mk