From 0022c4721fd65a2e3dede149ed62f4b5b7757c61 Mon Sep 17 00:00:00 2001 From: Sachin Hosmani Date: Mon, 15 Apr 2013 10:11:24 +1200 Subject: [PATCH 01/14] Bug 628785 - Firefox should define a value for extensions.getAddons.recommended.url so AddonRepoistory.retrieveRecommendedAddons() works. r=Unfocused --- browser/app/profile/firefox.js | 1 + 1 file changed, 1 insertion(+) diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index ae2642280ce..3dc3ef22751 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -42,6 +42,7 @@ pref("extensions.getAddons.getWithPerformance.url", "https://services.addons.moz pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/firefox/search?q=%TERMS%&platform=%OS%&appver=%VERSION%"); pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%/%COMPATIBILITY_MODE%?src=firefox"); pref("extensions.webservice.discoverURL", "https://services.addons.mozilla.org/%LOCALE%/firefox/discovery/pane/%VERSION%/%OS%/%COMPATIBILITY_MODE%"); +pref("extensions.getAddons.recommended.url", "https://services.addons.mozilla.org/%LOCALE%/%APP%/api/%API_VERSION%/list/recommended/all/%MAX_RESULTS%/%OS%/%VERSION%?src=firefox"); // Blocklist preferences pref("extensions.blocklist.enabled", true); From 572529ba461b25daecec129eb2f42b064af79751 Mon Sep 17 00:00:00 2001 From: Panos Astithas Date: Thu, 11 Apr 2013 10:22:50 +0300 Subject: [PATCH 02/14] Bug 860101 - payload._navPayload is a Window in 'will-navigate' event handler; r=harth --- browser/devtools/framework/Target.jsm | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/browser/devtools/framework/Target.jsm b/browser/devtools/framework/Target.jsm index 94b8c0988f8..d3b685726ad 100644 --- a/browser/devtools/framework/Target.jsm +++ b/browser/devtools/framework/Target.jsm @@ -322,13 +322,15 @@ TabTarget.prototype = { event.title = aPacket.title; // Send any stored event payload (DOMWindow or nsIRequest) for backwards // compatibility with non-remotable tools. - event._navPayload = this._navPayload; if (aPacket.state == "start") { + event._navPayload = this._navRequest; this.emit("will-navigate", event); + this._navRequest = null; } else { + event._navPayload = this._navWindow; this.emit("navigate", event); + this._navWindow = null; } - this._navPayload = null; }.bind(this); this.client.addListener("tabNavigated", this._onTabNavigated); }, @@ -467,7 +469,7 @@ TabWebProgressListener.prototype = { // Emit the event if the target is not remoted or store the payload for // later emission otherwise. if (this.target._client) { - this.target._navPayload = request; + this.target._navRequest = request; } else { this.target.emit("will-navigate", request); } @@ -485,7 +487,7 @@ TabWebProgressListener.prototype = { // Emit the event if the target is not remoted or store the payload for // later emission otherwise. if (this.target._client) { - this.target._navPayload = window; + this.target._navWindow = window; } else { this.target.emit("navigate", window); } @@ -500,6 +502,8 @@ TabWebProgressListener.prototype = { this.target.tab.linkedBrowser.removeProgressListener(this); } this.target._webProgressListener = null; + this.target._navRequest = null; + this.target._navWindow = null; this.target = null; } }; From 42e2ffce2efff150447b7fd0d2786cf6aab26787 Mon Sep 17 00:00:00 2001 From: Girish Sharma Date: Fri, 5 Apr 2013 00:00:24 +0530 Subject: [PATCH 03/14] Bug 851546 - Options panel for DevTools Toolbox, r=jwalker --- browser/app/profile/firefox.js | 1 + browser/devtools/framework/Toolbox.jsm | 65 ++++++-- browser/devtools/framework/gDevTools.jsm | 20 ++- browser/devtools/framework/test/Makefile.in | 1 + .../framework/test/browser_toolbox_options.js | 142 ++++++++++++++++++ browser/devtools/framework/test/head.js | 26 ++++ browser/devtools/framework/toolbox-options.js | 105 +++++++++++++ .../devtools/framework/toolbox-options.xul | 44 ++++++ browser/devtools/framework/toolbox.css | 14 ++ browser/devtools/framework/toolbox.xul | 18 +++ browser/devtools/jar.mn | 2 + browser/devtools/shared/theme-switching.js | 66 +++++++- .../shared/widgets/SideMenuWidget.jsm | 4 +- .../en-US/chrome/browser/devtools/toolbox.dtd | 17 ++- browser/themes/linux/devtools/toolbox.css | 38 +++++ browser/themes/osx/devtools/toolbox.css | 38 +++++ browser/themes/windows/devtools/toolbox.css | 38 +++++ 17 files changed, 614 insertions(+), 25 deletions(-) create mode 100644 browser/devtools/framework/test/browser_toolbox_options.js create mode 100644 browser/devtools/framework/toolbox-options.js create mode 100644 browser/devtools/framework/toolbox-options.xul diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 3dc3ef22751..271d8cd0b42 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1038,6 +1038,7 @@ pref("devtools.toolbox.host", "bottom"); pref("devtools.toolbox.selectedTool", "webconsole"); pref("devtools.toolbox.toolbarSpec", '["paintflashing toggle","tilt toggle","scratchpad","resize toggle"]'); pref("devtools.toolbox.sideEnabled", true); +pref("devtools.toolbox.disabledTools", "[]"); // Enable the Inspector pref("devtools.inspector.enabled", true); diff --git a/browser/devtools/framework/Toolbox.jsm b/browser/devtools/framework/Toolbox.jsm index 0d2209b3ff2..d31b8e3e7bb 100644 --- a/browser/devtools/framework/Toolbox.jsm +++ b/browser/devtools/framework/Toolbox.jsm @@ -135,7 +135,7 @@ this.Toolbox = function Toolbox(target, selectedTool, hostType) { selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL); } let definitions = gDevTools.getToolDefinitionMap(); - if (!definitions.get(selectedTool)) { + if (!definitions.get(selectedTool) && selectedTool != "options") { selectedTool = "webconsole"; } this._defaultToolId = selectedTool; @@ -263,6 +263,7 @@ Toolbox.prototype = { closeButton.addEventListener("command", this.destroy, true); this._buildDockButtons(); + this._buildOptions(); this._buildTabs(); this._buildButtons(); this._addKeysToWindow(); @@ -280,6 +281,21 @@ Toolbox.prototype = { return deferred.promise; }, + _buildOptions: function TBOX__buildOptions() { + this.optionsButton = this.doc.getElementById("toolbox-tab-options"); + this.optionsButton.addEventListener("command", function() { + this.selectTool("options"); + }.bind(this), false); + + let iframe = this.doc.getElementById("toolbox-panel-iframe-options"); + this._toolPanels.set("options", iframe); + + let key = this.doc.getElementById("toolbox-options-key"); + key.addEventListener("command", function(toolId) { + this.selectTool(toolId); + }.bind(this, "options"), true); + }, + /** * Adds the keys and commands to the Toolbox Window in window mode. */ @@ -288,7 +304,7 @@ Toolbox.prototype = { return; } let doc = this.doc.defaultView.parent.document; - for (let [id, toolDefinition] of gDevTools._tools) { + for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) { if (toolDefinition.key) { // Prevent multiple entries for the same tool. if (doc.getElementById("key_" + id)) { @@ -475,16 +491,30 @@ Toolbox.prototype = { break; } } - tabstrip.selectedIndex = index; + tabstrip.selectedItem = tab; // and select the right iframe let deck = this.doc.getElementById("toolbox-deck"); - deck.selectedIndex = index; + // offset by 1 due to options panel + if (id == "options") { + deck.selectedIndex = 0; + this.optionsButton.setAttribute("checked", true); + } + else { + deck.selectedIndex = index != -1 ? index + 1: -1; + this.optionsButton.removeAttribute("checked"); + } let definition = gDevTools.getToolDefinitionMap().get(id); this._currentToolId = id; + let resolveSelected = panel => { + this.emit("select", id); + this.emit(id + "-selected", panel); + deferred.resolve(panel); + }; + let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); if (!iframe) { iframe = this.doc.createElement("iframe"); @@ -505,11 +535,9 @@ Toolbox.prototype = { this._toolPanels.set(id, panel); this.emit(id + "-ready", panel); - this.emit("select", id); - this.emit(id + "-selected", panel); gDevTools.emit(id + "-ready", this, panel); - deferred.resolve(panel); + resolveSelected(panel); }.bind(this)); }.bind(this); @@ -518,14 +546,22 @@ Toolbox.prototype = { } else { let panel = this._toolPanels.get(id); // only emit 'select' event if the iframe has been loaded - if (panel) { - this.emit("select", id); - this.emit(id + "-selected", panel); - deferred.resolve(panel); + if (panel && (!panel.contentDocument || + panel.contentDocument.readyState == "complete")) { + resolveSelected(panel); + } + else if (panel) { + let boundLoad = function() { + panel.removeEventListener("DOMContentLoaded", boundLoad, true); + resolveSelected(panel); + }; + panel.addEventListener("DOMContentLoaded", boundLoad, true); } } - Services.prefs.setCharPref(this._prefs.LAST_TOOL, id); + if (id != "options") { + Services.prefs.setCharPref(this._prefs.LAST_TOOL, id); + } return deferred.promise; }, @@ -543,8 +579,8 @@ Toolbox.prototype = { _refreshHostTitle: function TBOX_refreshHostTitle() { let toolName; let toolId = this.currentToolId; - if (toolId) { - let toolDef = gDevTools.getToolDefinitionMap().get(toolId); + let toolDef = gDevTools.getToolDefinitionMap().get(toolId); + if (toolDef) { toolName = toolDef.label; } else { // no tool is selected @@ -710,6 +746,7 @@ Toolbox.prototype = { let outstanding = []; + this._toolPanels.delete("options"); for (let [id, panel] of this._toolPanels) { outstanding.push(panel.destroy()); } diff --git a/browser/devtools/framework/gDevTools.jsm b/browser/devtools/framework/gDevTools.jsm index c5820d7431a..7bc6f659d0b 100644 --- a/browser/devtools/framework/gDevTools.jsm +++ b/browser/devtools/framework/gDevTools.jsm @@ -97,6 +97,20 @@ DevTools.prototype = { } }, + getDefaultTools: function DT_getDefaultTools() { + return defaultTools; + }, + + getAdditionalTools: function DT_getAdditionalTools() { + let tools = []; + for (let [key, value] of this._tools) { + if (defaultTools.indexOf(value) == -1) { + tools.push(value); + } + } + return tools; + }, + /** * Allow ToolBoxes to get at the list of tools that they should populate * themselves with. @@ -106,6 +120,10 @@ DevTools.prototype = { */ getToolDefinitionMap: function DT_getToolDefinitionMap() { let tools = new Map(); + let disabledTools = []; + try { + disabledTools = JSON.parse(Services.prefs.getCharPref("devtools.toolbox.disabledTools")); + } catch(ex) {} for (let [key, value] of this._tools) { let enabled; @@ -116,7 +134,7 @@ DevTools.prototype = { enabled = true; } - if (enabled) { + if (enabled && disabledTools.indexOf(key) == -1) { tools.set(key, value); } } diff --git a/browser/devtools/framework/test/Makefile.in b/browser/devtools/framework/test/Makefile.in index 5cc45851928..d4c667541d5 100644 --- a/browser/devtools/framework/test/Makefile.in +++ b/browser/devtools/framework/test/Makefile.in @@ -23,6 +23,7 @@ MOCHITEST_BROWSER_FILES = \ browser_toolbox_sidebar.js \ browser_toolbox_window_shortcuts.js \ browser_toolbox_window_title_changes.js \ + browser_toolbox_options.js \ $(NULL) include $(topsrcdir)/config/rules.mk diff --git a/browser/devtools/framework/test/browser_toolbox_options.js b/browser/devtools/framework/test/browser_toolbox_options.js new file mode 100644 index 00000000000..7fbd4efdf71 --- /dev/null +++ b/browser/devtools/framework/test/browser_toolbox_options.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let tempScope = {}; +Cu.import("resource:///modules/devtools/Target.jsm", tempScope); +let TargetFactory = tempScope.TargetFactory; +let doc = null, toolbox = null, panelWin = null, index = 0, prefValues = [], prefNodes = []; + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + let target = TargetFactory.forTab(gBrowser.selectedTab); + + gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) { + gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true); + gDevTools.showToolbox(target).then(testSelectTool); + }, true); + + content.location = "data:text/html,test for dynamically registering and unregistering tools"; +} + +function testSelectTool(aToolbox) { + toolbox = aToolbox; + doc = toolbox.doc; + toolbox.once("options-selected", testOptionsShortcut); + toolbox.selectTool("options"); +} + +function testOptionsShortcut() { + ok(true, "Toolbox selected via selectTool method"); + toolbox.once("options-selected", testOptionsButtonClick); + toolbox.selectTool("webconsole") + .then(() => synthesizeKeyFromKeyTag("toolbox-options-key", doc)); +} + +function testOptionsButtonClick() { + ok(true, "Toolbox selected via shortcut"); + toolbox.once("options-selected", testOptions); + toolbox.selectTool("webconsole") + .then(() => doc.getElementById("toolbox-tab-options").click()); +} + +function testOptions(event, iframe) { + ok(true, "Toolbox selected via button click"); + panelWin = iframe.contentWindow; + let panelDoc = iframe.contentDocument; + // Testing pref changes + let prefCheckboxes = panelDoc.querySelectorAll("checkbox[data-pref]"); + for (let checkbox of prefCheckboxes) { + prefNodes.push(checkbox); + prefValues.push(Services.prefs.getBoolPref(checkbox.getAttribute("data-pref"))); + } + // Do again with opposite values to reset prefs + for (let checkbox of prefCheckboxes) { + prefNodes.push(checkbox); + prefValues.push(!Services.prefs.getBoolPref(checkbox.getAttribute("data-pref"))); + } + testMouseClicks(); +} + +function testMouseClicks() { + if (index == prefValues.length) { + checkTools(); + return; + } + gDevTools.once("pref-changed", prefChanged); + info("Click event synthesized for index " + index); + EventUtils.synthesizeMouse(prefNodes[index], 10, 10, {}, panelWin); +} + +function prefChanged(event, data) { + if (data.pref == prefNodes[index].getAttribute("data-pref")) { + ok(true, "Correct pref was changed"); + is(data.oldValue, prefValues[index], "Previous value is correct"); + is(data.newValue, !prefValues[index], "New value is correct"); + index++; + testMouseClicks(); + return; + } + ok(false, "Pref was not changed correctly"); + cleanup(); +} + +function checkTools() { + let toolsPref = panelWin.document.querySelectorAll("#default-tools-box > checkbox"); + prefNodes = []; + index = 0; + for (let tool of toolsPref) { + prefNodes.push(tool); + } + toggleTools(); +} + +function toggleTools() { + if (index < prefNodes.length) { + gDevTools.once("tool-unregistered", checkUnregistered); + EventUtils.synthesizeMouse(prefNodes[index], 10, 10, {}, panelWin); + } + else if (index < 2*prefNodes.length) { + gDevTools.once("tool-registered", checkRegistered); + EventUtils.synthesizeMouse(prefNodes[index - prefNodes.length], 10, 10, {}, panelWin); + } + else { + cleanup(); + return; + } +} + +function checkUnregistered(event, data) { + if (data == prefNodes[index].getAttribute("id")) { + ok(true, "Correct tool removed"); + // checking tab on the toolbox + ok(!doc.getElementById("toolbox-tab-" + data), "Tab removed for " + data); + index++; + toggleTools(); + return; + } + ok(false, "Something went wrong, " + data + " was not unregistered"); + cleanup(); +} + +function checkRegistered(event, data) { + if (data == prefNodes[index - prefNodes.length].getAttribute("id")) { + ok(true, "Correct tool added back"); + // checking tab on the toolbox + ok(doc.getElementById("toolbox-tab-" + data), "Tab added back for " + data); + index++; + toggleTools(); + return; + } + ok(false, "Something went wrong, " + data + " was not registered back"); + cleanup(); +} + +function cleanup() { + toolbox.destroy().then(function() { + gBrowser.removeCurrentTab(); + toolbox = doc = prefNodes = prefValues = panelWin = null; + finish(); + }); +} diff --git a/browser/devtools/framework/test/head.js b/browser/devtools/framework/test/head.js index 66df4ab8f0e..144a9ed5d48 100644 --- a/browser/devtools/framework/test/head.js +++ b/browser/devtools/framework/test/head.js @@ -47,3 +47,29 @@ registerCleanupFunction(function tearDown() { gBrowser.removeCurrentTab(); } }); + +function synthesizeKeyFromKeyTag(aKeyId, document) { + let key = document.getElementById(aKeyId); + isnot(key, null, "Successfully retrieved the node"); + + let modifiersAttr = key.getAttribute("modifiers"); + + let name = null; + + if (key.getAttribute("keycode")) + name = key.getAttribute("keycode"); + else if (key.getAttribute("key")) + name = key.getAttribute("key"); + + isnot(name, null, "Successfully retrieved keycode/key"); + + let modifiers = { + shiftKey: modifiersAttr.match("shift"), + ctrlKey: modifiersAttr.match("ctrl"), + altKey: modifiersAttr.match("alt"), + metaKey: modifiersAttr.match("meta"), + accelKey: modifiersAttr.match("accel") + } + + EventUtils.synthesizeKey(name, modifiers); +} diff --git a/browser/devtools/framework/toolbox-options.js b/browser/devtools/framework/toolbox-options.js new file mode 100644 index 00000000000..fc452552998 --- /dev/null +++ b/browser/devtools/framework/toolbox-options.js @@ -0,0 +1,105 @@ + +const { utils: Cu } = Components; +const DISABLED_TOOLS = "devtools.toolbox.disabledTools"; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/devtools/gDevTools.jsm"); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + setupToolsList(); + populatePreferences(); +}); + +function setupToolsList() { + let disabledTools = []; + try { + disabledTools = JSON.parse(Services.prefs.getCharPref(DISABLED_TOOLS)); + } catch(ex) { + Cu.reportError("Error parsing pref " + DISABLED_TOOLS + " as JSON."); + } + let defaultToolsBox = document.getElementById("default-tools-box"); + let additionalToolsBox = document.getElementById("additional-tools-box"); + + defaultToolsBox.textContent = ""; + additionalToolsBox.textContent = ""; + + let onCheckboxClick = function(id) { + if (disabledTools.indexOf(id) > -1) { + disabledTools.splice(disabledTools.indexOf(id), 1); + Services.prefs.setCharPref(DISABLED_TOOLS, JSON.stringify(disabledTools)); + gDevTools.emit("tool-registered", id); + } + else { + disabledTools.push(id); + Services.prefs.setCharPref(DISABLED_TOOLS, JSON.stringify(disabledTools)); + gDevTools.emit("tool-unregistered", id); + } + }; + + // Populating the default tools lists + for (let tool of gDevTools.getDefaultTools()) { + let checkbox = document.createElement("checkbox"); + checkbox.setAttribute("id", tool.id); + checkbox.setAttribute("label", tool.label); + checkbox.setAttribute("tooltiptext", tool.tooltip || ""); + checkbox.setAttribute("checked", disabledTools.indexOf(tool.id) == -1); + checkbox.addEventListener("command", onCheckboxClick.bind(null, tool.id)); + defaultToolsBox.appendChild(checkbox); + } + + // Populating the additional tools list that came from add-ons. + let atleastOneAddon = false; + for (let tool of gDevTools.getAdditionalTools()) { + atleastOneAddon = true; + let checkbox = document.createElement("checkbox"); + checkbox.setAttribute("id", tool.id); + checkbox.setAttribute("label", tool.label); + checkbox.setAttribute("tooltiptext", tool.tooltip || ""); + checkbox.setAttribute("checked", disabledTools.indexOf(tool.id) == -1); + checkbox.addEventListener("command", onCheckboxClick.bind(null, tool.id)); + additionalToolsBox.appendChild(checkbox); + } + + if (!atleastOneAddon) { + additionalToolsBox.style.display = "none"; + additionalToolsBox.previousSibling.style.display = "none"; + } + + window.focus(); +} + +function populatePreferences() { + let prefCheckboxes = document.querySelectorAll("checkbox[data-pref]"); + for (let checkbox of prefCheckboxes) { + checkbox.checked = Services.prefs.getBoolPref(checkbox.getAttribute("data-pref")); + checkbox.addEventListener("command", function() { + let data = { + pref: this.getAttribute("data-pref"), + newValue: this.checked + }; + data.oldValue = Services.prefs.getBoolPref(data.pref); + Services.prefs.setBoolPref(data.pref, data.newValue); + gDevTools.emit("pref-changed", data); + }.bind(checkbox)); + } + let prefRadiogroups = document.querySelectorAll("radiogroup[data-pref]"); + for (let radiogroup of prefRadiogroups) { + let selectedValue = Services.prefs.getCharPref(radiogroup.getAttribute("data-pref")); + for (let radio of radiogroup.childNodes) { + radiogroup.selectedIndex = -1; + if (radio.getAttribute("value") == selectedValue) { + radiogroup.selectedItem = radio; + break; + } + } + radiogroup.addEventListener("select", function() { + let data = { + pref: this.getAttribute("data-pref"), + newValue: this.selectedItem.getAttribute("value") + }; + data.oldValue = Services.prefs.getCharPref(data.pref); + Services.prefs.setCharPref(data.pref, data.newValue); + gDevTools.emit("pref-changed", data); + }.bind(radiogroup)); + } +} diff --git a/browser/devtools/framework/toolbox-options.xul b/browser/devtools/framework/toolbox-options.xul new file mode 100644 index 00000000000..dbd4e835761 --- /dev/null +++ b/browser/devtools/framework/toolbox-options.xul @@ -0,0 +1,44 @@ + + + + %toolboxDTD; +]> + + + + + + + + + + + + diff --git a/browser/devtools/framework/toolbox.css b/browser/devtools/framework/toolbox.css index 03a48c9aaab..efd054db508 100644 --- a/browser/devtools/framework/toolbox.css +++ b/browser/devtools/framework/toolbox.css @@ -13,3 +13,17 @@ .command-button > .toolbarbutton-text { display: none; } + +#options-panel { + overflow-y: auto; + display: block; +} + +.options-vertical-pane { + display: inline; + float: left; +} + +.options-vertical-pane > label { + display: block; +} diff --git a/browser/devtools/framework/toolbox.xul b/browser/devtools/framework/toolbox.xul index 297e0c296ad..16c26961592 100644 --- a/browser/devtools/framework/toolbox.xul +++ b/browser/devtools/framework/toolbox.xul @@ -18,6 +18,12 @@ + + + @@ -28,6 +34,10 @@ #endif + @@ -41,6 +51,14 @@ #endif + + + diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn index dbed935bab4..8c49610729e 100644 --- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -57,6 +57,8 @@ browser.jar: content/browser/devtools/commandlineoutput.xhtml (commandline/commandlineoutput.xhtml) content/browser/devtools/commandlinetooltip.xhtml (commandline/commandlinetooltip.xhtml) content/browser/devtools/framework/toolbox-window.xul (framework/toolbox-window.xul) + content/browser/devtools/framework/toolbox-options.xul (framework/toolbox-options.xul) + content/browser/devtools/framework/toolbox-options.js (framework/toolbox-options.js) * content/browser/devtools/framework/toolbox.xul (framework/toolbox.xul) content/browser/devtools/framework/toolbox.css (framework/toolbox.css) content/browser/devtools/inspector/inspector.xul (inspector/inspector.xul) diff --git a/browser/devtools/shared/theme-switching.js b/browser/devtools/shared/theme-switching.js index 044ae9bde16..ce25a821790 100644 --- a/browser/devtools/shared/theme-switching.js +++ b/browser/devtools/shared/theme-switching.js @@ -3,15 +3,65 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ (function() { + const DEVTOOLS_SKIN_URL = "chrome://browser/skin/devtools/"; + + function forceStyle() { + let computedStyle = window.getComputedStyle(document.documentElement); + if (!computedStyle) { + // Null when documentElement is not ready. This method is anyways not + // required then as scrollbars would be in their state without flushing. + return; + } + let display = computedStyle.display; // Save display value + document.documentElement.style.display = "none"; + window.getComputedStyle(document.documentElement).display; // Flush + document.documentElement.style.display = display; // Restore + } + + function switchTheme(theme, oldTheme) { + let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + if (oldTheme && theme != oldTheme) { + let old_theme_url = Services.io.newURI(DEVTOOLS_SKIN_URL + oldTheme + + "-theme.css", null, null); + try { + winUtils.removeSheet(old_theme_url, window.AUTHOR_SHEET); + } catch(ex) {} + } + let theme_url = Services.io.newURI( + + theme + "-theme.css", null, null); + winUtils.loadSheet(theme_url, window.AUTHOR_SHEET); + let scrollbar_url = + Services.io.newURI(DEVTOOLS_SKIN_URL + "floating-scrollbars-light.css", + null, null); + if (theme == "dark") { + winUtils.loadSheet(scrollbar_url, window.AGENT_SHEET); + forceStyle(); + } + else if (oldTheme == "dark") { + try { + winUtils.removeSheet(scrollbar_url, window.AGENT_SHEET); + } catch(ex) {} + forceStyle(); + } + document.documentElement.classList.remove("theme-" + oldTheme); + document.documentElement.classList.add("theme-" + theme); + } + + function handlePrefChange(event, data) { + switch(data.pref) { + case "devtools.theme": + switchTheme(data.newValue, data.oldValue); + } + } + const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/Services.jsm"); + Cu.import("resource:///modules/devtools/gDevTools.jsm"); let theme = Services.prefs.getCharPref("devtools.theme"); - let theme_url = Services.io.newURI("chrome://browser/skin/devtools/" + theme + "-theme.css", null, null); - let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); - winUtils.loadSheet(theme_url, window.AUTHOR_SHEET); - if (theme == "dark") { - let scrollbar_url = Services.io.newURI("chrome://browser/skin/devtools/floating-scrollbars-light.css", null, null); - winUtils.loadSheet(scrollbar_url, window.AGENT_SHEET); - } - document.documentElement.classList.add("theme-" + theme); + switchTheme(theme); + gDevTools.on("pref-changed", handlePrefChange); + window.addEventListener("unload", function() { + gDevTools.off("pref-changed", handlePrefChange); + }); })() diff --git a/browser/devtools/shared/widgets/SideMenuWidget.jsm b/browser/devtools/shared/widgets/SideMenuWidget.jsm index d11c898f23b..5e98ee6a88a 100644 --- a/browser/devtools/shared/widgets/SideMenuWidget.jsm +++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm @@ -117,7 +117,9 @@ SideMenuWidget.prototype = { * The element associated with the displayed item. */ removeChild: function SMW_removeChild(aChild) { - aChild.parentNode.removeChild(aChild); + // Remove the item itself, not the contents. + let item = aChild.parentNode; + item.parentNode.removeChild(item); this._orderedMenuElementsArray.splice( this._orderedMenuElementsArray.indexOf(aChild), 1); diff --git a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd index 57efe9e36df..86339ba8713 100644 --- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd @@ -4,4 +4,19 @@ - + + + + + + + + + + + + + + + + diff --git a/browser/themes/linux/devtools/toolbox.css b/browser/themes/linux/devtools/toolbox.css index ed42d821296..42807bb0aef 100644 --- a/browser/themes/linux/devtools/toolbox.css +++ b/browser/themes/linux/devtools/toolbox.css @@ -128,6 +128,19 @@ -moz-image-region: rect(0px, 48px, 16px, 32px); } +#toolbox-tab-options { + list-style-image: url("chrome://browser/skin/devtools/option-icon.png"); + -moz-image-region: rect(0px 16px 16px 0px); +} + +#toolbox-tab-options[checked=true] { + -moz-image-region: rect(0px 32px 16px 16px); +} + +#toolbox-tab-options > image { + opacity: 1; + -moz-margin-start: 0; +} /* Tabs */ @@ -224,3 +237,28 @@ background-position: top right, top left, left, right; box-shadow: 1px -1px 0 hsla(206,37%,4%,.2) inset; } + +#options-panel { + background-image: url("chrome://browser/skin/newtab/noise.png"); +} + +.options-vertical-pane { + margin: 15px; + width: calc(50% - 30px); + min-width: 400px; + -moz-padding-start: 5px; +} + +.options-vertical-pane > label { + padding: 5px 0; + font-size: 1.4rem; +} + +.options-groupbox { + -moz-margin-start: 15px; + padding: 4px; +} + +.options-groupbox > * { + padding: 2px; +} diff --git a/browser/themes/osx/devtools/toolbox.css b/browser/themes/osx/devtools/toolbox.css index cca78b2264b..4866fcc4592 100644 --- a/browser/themes/osx/devtools/toolbox.css +++ b/browser/themes/osx/devtools/toolbox.css @@ -116,6 +116,19 @@ -moz-image-region: rect(0px, 48px, 16px, 32px); } +#toolbox-tab-options { + list-style-image: url("chrome://browser/skin/devtools/option-icon.png"); + -moz-image-region: rect(0px 16px 16px 0px); +} + +#toolbox-tab-options[checked=true] { + -moz-image-region: rect(0px 32px 16px 16px); +} + +#toolbox-tab-options > image { + opacity: 1; + -moz-margin-start: 0; +} /* Tabs */ @@ -211,3 +224,28 @@ background-position: top right, top left, left, right; box-shadow: 1px -1px 0 hsla(206,37%,4%,.2) inset; } + +#options-panel { + background-image: url("chrome://browser/skin/newtab/noise.png"); +} + +.options-vertical-pane { + margin: 15px; + width: calc(50% - 30px); + min-width: 400px; + -moz-padding-start: 5px; +} + +.options-vertical-pane > label { + padding: 5px 0; + font-size: 1.4rem; +} + +.options-groupbox { + -moz-margin-start: 15px; + padding: 4px; +} + +.options-groupbox > * { + padding: 2px; +} diff --git a/browser/themes/windows/devtools/toolbox.css b/browser/themes/windows/devtools/toolbox.css index 2361d5f196e..4540653bf55 100644 --- a/browser/themes/windows/devtools/toolbox.css +++ b/browser/themes/windows/devtools/toolbox.css @@ -131,6 +131,19 @@ -moz-image-region: rect(0px, 48px, 16px, 32px); } +#toolbox-tab-options { + list-style-image: url("chrome://browser/skin/devtools/option-icon.png"); + -moz-image-region: rect(0px 16px 16px 0px); +} + +#toolbox-tab-options[checked=true] { + -moz-image-region: rect(0px 32px 16px 16px); +} + +#toolbox-tab-options > image { + opacity: 1; + -moz-margin-start: 0; +} /* Tabs */ @@ -220,3 +233,28 @@ background-position: top right, top left, left, right; box-shadow: 1px -1px 0 hsla(206,37%,4%,.2) inset; } + +#options-panel { + background-image: url("chrome://browser/skin/newtab/noise.png"); +} + +.options-vertical-pane { + margin: 15px; + width: calc(50% - 30px); + min-width: 400px; + -moz-padding-start: 5px; +} + +.options-vertical-pane > label { + padding: 5px 0; + font-size: 1.4rem; +} + +.options-groupbox { + -moz-margin-start: 15px; + padding: 4px; +} + +.options-groupbox > * { + padding: 2px; +} From 79d329ae2fc60abe335a1b5fbdf995e7d54a3681 Mon Sep 17 00:00:00 2001 From: Panos Astithas Date: Mon, 15 Apr 2013 11:51:19 +0300 Subject: [PATCH 04/14] Backed out changeset 371ecfa8df92 (bug 851546) for breakage --- browser/app/profile/firefox.js | 1 - browser/devtools/framework/Toolbox.jsm | 65 ++------ browser/devtools/framework/gDevTools.jsm | 20 +-- browser/devtools/framework/test/Makefile.in | 1 - .../framework/test/browser_toolbox_options.js | 142 ------------------ browser/devtools/framework/test/head.js | 26 ---- browser/devtools/framework/toolbox-options.js | 105 ------------- .../devtools/framework/toolbox-options.xul | 44 ------ browser/devtools/framework/toolbox.css | 14 -- browser/devtools/framework/toolbox.xul | 18 --- browser/devtools/jar.mn | 2 - browser/devtools/shared/theme-switching.js | 66 +------- .../shared/widgets/SideMenuWidget.jsm | 4 +- .../en-US/chrome/browser/devtools/toolbox.dtd | 17 +-- browser/themes/linux/devtools/toolbox.css | 38 ----- browser/themes/osx/devtools/toolbox.css | 38 ----- browser/themes/windows/devtools/toolbox.css | 38 ----- 17 files changed, 25 insertions(+), 614 deletions(-) delete mode 100644 browser/devtools/framework/test/browser_toolbox_options.js delete mode 100644 browser/devtools/framework/toolbox-options.js delete mode 100644 browser/devtools/framework/toolbox-options.xul diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 271d8cd0b42..3dc3ef22751 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1038,7 +1038,6 @@ pref("devtools.toolbox.host", "bottom"); pref("devtools.toolbox.selectedTool", "webconsole"); pref("devtools.toolbox.toolbarSpec", '["paintflashing toggle","tilt toggle","scratchpad","resize toggle"]'); pref("devtools.toolbox.sideEnabled", true); -pref("devtools.toolbox.disabledTools", "[]"); // Enable the Inspector pref("devtools.inspector.enabled", true); diff --git a/browser/devtools/framework/Toolbox.jsm b/browser/devtools/framework/Toolbox.jsm index d31b8e3e7bb..0d2209b3ff2 100644 --- a/browser/devtools/framework/Toolbox.jsm +++ b/browser/devtools/framework/Toolbox.jsm @@ -135,7 +135,7 @@ this.Toolbox = function Toolbox(target, selectedTool, hostType) { selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL); } let definitions = gDevTools.getToolDefinitionMap(); - if (!definitions.get(selectedTool) && selectedTool != "options") { + if (!definitions.get(selectedTool)) { selectedTool = "webconsole"; } this._defaultToolId = selectedTool; @@ -263,7 +263,6 @@ Toolbox.prototype = { closeButton.addEventListener("command", this.destroy, true); this._buildDockButtons(); - this._buildOptions(); this._buildTabs(); this._buildButtons(); this._addKeysToWindow(); @@ -281,21 +280,6 @@ Toolbox.prototype = { return deferred.promise; }, - _buildOptions: function TBOX__buildOptions() { - this.optionsButton = this.doc.getElementById("toolbox-tab-options"); - this.optionsButton.addEventListener("command", function() { - this.selectTool("options"); - }.bind(this), false); - - let iframe = this.doc.getElementById("toolbox-panel-iframe-options"); - this._toolPanels.set("options", iframe); - - let key = this.doc.getElementById("toolbox-options-key"); - key.addEventListener("command", function(toolId) { - this.selectTool(toolId); - }.bind(this, "options"), true); - }, - /** * Adds the keys and commands to the Toolbox Window in window mode. */ @@ -304,7 +288,7 @@ Toolbox.prototype = { return; } let doc = this.doc.defaultView.parent.document; - for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) { + for (let [id, toolDefinition] of gDevTools._tools) { if (toolDefinition.key) { // Prevent multiple entries for the same tool. if (doc.getElementById("key_" + id)) { @@ -491,30 +475,16 @@ Toolbox.prototype = { break; } } - tabstrip.selectedItem = tab; + tabstrip.selectedIndex = index; // and select the right iframe let deck = this.doc.getElementById("toolbox-deck"); - // offset by 1 due to options panel - if (id == "options") { - deck.selectedIndex = 0; - this.optionsButton.setAttribute("checked", true); - } - else { - deck.selectedIndex = index != -1 ? index + 1: -1; - this.optionsButton.removeAttribute("checked"); - } + deck.selectedIndex = index; let definition = gDevTools.getToolDefinitionMap().get(id); this._currentToolId = id; - let resolveSelected = panel => { - this.emit("select", id); - this.emit(id + "-selected", panel); - deferred.resolve(panel); - }; - let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); if (!iframe) { iframe = this.doc.createElement("iframe"); @@ -535,9 +505,11 @@ Toolbox.prototype = { this._toolPanels.set(id, panel); this.emit(id + "-ready", panel); + this.emit("select", id); + this.emit(id + "-selected", panel); gDevTools.emit(id + "-ready", this, panel); - resolveSelected(panel); + deferred.resolve(panel); }.bind(this)); }.bind(this); @@ -546,22 +518,14 @@ Toolbox.prototype = { } else { let panel = this._toolPanels.get(id); // only emit 'select' event if the iframe has been loaded - if (panel && (!panel.contentDocument || - panel.contentDocument.readyState == "complete")) { - resolveSelected(panel); - } - else if (panel) { - let boundLoad = function() { - panel.removeEventListener("DOMContentLoaded", boundLoad, true); - resolveSelected(panel); - }; - panel.addEventListener("DOMContentLoaded", boundLoad, true); + if (panel) { + this.emit("select", id); + this.emit(id + "-selected", panel); + deferred.resolve(panel); } } - if (id != "options") { - Services.prefs.setCharPref(this._prefs.LAST_TOOL, id); - } + Services.prefs.setCharPref(this._prefs.LAST_TOOL, id); return deferred.promise; }, @@ -579,8 +543,8 @@ Toolbox.prototype = { _refreshHostTitle: function TBOX_refreshHostTitle() { let toolName; let toolId = this.currentToolId; - let toolDef = gDevTools.getToolDefinitionMap().get(toolId); - if (toolDef) { + if (toolId) { + let toolDef = gDevTools.getToolDefinitionMap().get(toolId); toolName = toolDef.label; } else { // no tool is selected @@ -746,7 +710,6 @@ Toolbox.prototype = { let outstanding = []; - this._toolPanels.delete("options"); for (let [id, panel] of this._toolPanels) { outstanding.push(panel.destroy()); } diff --git a/browser/devtools/framework/gDevTools.jsm b/browser/devtools/framework/gDevTools.jsm index 7bc6f659d0b..c5820d7431a 100644 --- a/browser/devtools/framework/gDevTools.jsm +++ b/browser/devtools/framework/gDevTools.jsm @@ -97,20 +97,6 @@ DevTools.prototype = { } }, - getDefaultTools: function DT_getDefaultTools() { - return defaultTools; - }, - - getAdditionalTools: function DT_getAdditionalTools() { - let tools = []; - for (let [key, value] of this._tools) { - if (defaultTools.indexOf(value) == -1) { - tools.push(value); - } - } - return tools; - }, - /** * Allow ToolBoxes to get at the list of tools that they should populate * themselves with. @@ -120,10 +106,6 @@ DevTools.prototype = { */ getToolDefinitionMap: function DT_getToolDefinitionMap() { let tools = new Map(); - let disabledTools = []; - try { - disabledTools = JSON.parse(Services.prefs.getCharPref("devtools.toolbox.disabledTools")); - } catch(ex) {} for (let [key, value] of this._tools) { let enabled; @@ -134,7 +116,7 @@ DevTools.prototype = { enabled = true; } - if (enabled && disabledTools.indexOf(key) == -1) { + if (enabled) { tools.set(key, value); } } diff --git a/browser/devtools/framework/test/Makefile.in b/browser/devtools/framework/test/Makefile.in index d4c667541d5..5cc45851928 100644 --- a/browser/devtools/framework/test/Makefile.in +++ b/browser/devtools/framework/test/Makefile.in @@ -23,7 +23,6 @@ MOCHITEST_BROWSER_FILES = \ browser_toolbox_sidebar.js \ browser_toolbox_window_shortcuts.js \ browser_toolbox_window_title_changes.js \ - browser_toolbox_options.js \ $(NULL) include $(topsrcdir)/config/rules.mk diff --git a/browser/devtools/framework/test/browser_toolbox_options.js b/browser/devtools/framework/test/browser_toolbox_options.js deleted file mode 100644 index 7fbd4efdf71..00000000000 --- a/browser/devtools/framework/test/browser_toolbox_options.js +++ /dev/null @@ -1,142 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -let tempScope = {}; -Cu.import("resource:///modules/devtools/Target.jsm", tempScope); -let TargetFactory = tempScope.TargetFactory; -let doc = null, toolbox = null, panelWin = null, index = 0, prefValues = [], prefNodes = []; - -function test() { - waitForExplicitFinish(); - - gBrowser.selectedTab = gBrowser.addTab(); - let target = TargetFactory.forTab(gBrowser.selectedTab); - - gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) { - gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true); - gDevTools.showToolbox(target).then(testSelectTool); - }, true); - - content.location = "data:text/html,test for dynamically registering and unregistering tools"; -} - -function testSelectTool(aToolbox) { - toolbox = aToolbox; - doc = toolbox.doc; - toolbox.once("options-selected", testOptionsShortcut); - toolbox.selectTool("options"); -} - -function testOptionsShortcut() { - ok(true, "Toolbox selected via selectTool method"); - toolbox.once("options-selected", testOptionsButtonClick); - toolbox.selectTool("webconsole") - .then(() => synthesizeKeyFromKeyTag("toolbox-options-key", doc)); -} - -function testOptionsButtonClick() { - ok(true, "Toolbox selected via shortcut"); - toolbox.once("options-selected", testOptions); - toolbox.selectTool("webconsole") - .then(() => doc.getElementById("toolbox-tab-options").click()); -} - -function testOptions(event, iframe) { - ok(true, "Toolbox selected via button click"); - panelWin = iframe.contentWindow; - let panelDoc = iframe.contentDocument; - // Testing pref changes - let prefCheckboxes = panelDoc.querySelectorAll("checkbox[data-pref]"); - for (let checkbox of prefCheckboxes) { - prefNodes.push(checkbox); - prefValues.push(Services.prefs.getBoolPref(checkbox.getAttribute("data-pref"))); - } - // Do again with opposite values to reset prefs - for (let checkbox of prefCheckboxes) { - prefNodes.push(checkbox); - prefValues.push(!Services.prefs.getBoolPref(checkbox.getAttribute("data-pref"))); - } - testMouseClicks(); -} - -function testMouseClicks() { - if (index == prefValues.length) { - checkTools(); - return; - } - gDevTools.once("pref-changed", prefChanged); - info("Click event synthesized for index " + index); - EventUtils.synthesizeMouse(prefNodes[index], 10, 10, {}, panelWin); -} - -function prefChanged(event, data) { - if (data.pref == prefNodes[index].getAttribute("data-pref")) { - ok(true, "Correct pref was changed"); - is(data.oldValue, prefValues[index], "Previous value is correct"); - is(data.newValue, !prefValues[index], "New value is correct"); - index++; - testMouseClicks(); - return; - } - ok(false, "Pref was not changed correctly"); - cleanup(); -} - -function checkTools() { - let toolsPref = panelWin.document.querySelectorAll("#default-tools-box > checkbox"); - prefNodes = []; - index = 0; - for (let tool of toolsPref) { - prefNodes.push(tool); - } - toggleTools(); -} - -function toggleTools() { - if (index < prefNodes.length) { - gDevTools.once("tool-unregistered", checkUnregistered); - EventUtils.synthesizeMouse(prefNodes[index], 10, 10, {}, panelWin); - } - else if (index < 2*prefNodes.length) { - gDevTools.once("tool-registered", checkRegistered); - EventUtils.synthesizeMouse(prefNodes[index - prefNodes.length], 10, 10, {}, panelWin); - } - else { - cleanup(); - return; - } -} - -function checkUnregistered(event, data) { - if (data == prefNodes[index].getAttribute("id")) { - ok(true, "Correct tool removed"); - // checking tab on the toolbox - ok(!doc.getElementById("toolbox-tab-" + data), "Tab removed for " + data); - index++; - toggleTools(); - return; - } - ok(false, "Something went wrong, " + data + " was not unregistered"); - cleanup(); -} - -function checkRegistered(event, data) { - if (data == prefNodes[index - prefNodes.length].getAttribute("id")) { - ok(true, "Correct tool added back"); - // checking tab on the toolbox - ok(doc.getElementById("toolbox-tab-" + data), "Tab added back for " + data); - index++; - toggleTools(); - return; - } - ok(false, "Something went wrong, " + data + " was not registered back"); - cleanup(); -} - -function cleanup() { - toolbox.destroy().then(function() { - gBrowser.removeCurrentTab(); - toolbox = doc = prefNodes = prefValues = panelWin = null; - finish(); - }); -} diff --git a/browser/devtools/framework/test/head.js b/browser/devtools/framework/test/head.js index 144a9ed5d48..66df4ab8f0e 100644 --- a/browser/devtools/framework/test/head.js +++ b/browser/devtools/framework/test/head.js @@ -47,29 +47,3 @@ registerCleanupFunction(function tearDown() { gBrowser.removeCurrentTab(); } }); - -function synthesizeKeyFromKeyTag(aKeyId, document) { - let key = document.getElementById(aKeyId); - isnot(key, null, "Successfully retrieved the node"); - - let modifiersAttr = key.getAttribute("modifiers"); - - let name = null; - - if (key.getAttribute("keycode")) - name = key.getAttribute("keycode"); - else if (key.getAttribute("key")) - name = key.getAttribute("key"); - - isnot(name, null, "Successfully retrieved keycode/key"); - - let modifiers = { - shiftKey: modifiersAttr.match("shift"), - ctrlKey: modifiersAttr.match("ctrl"), - altKey: modifiersAttr.match("alt"), - metaKey: modifiersAttr.match("meta"), - accelKey: modifiersAttr.match("accel") - } - - EventUtils.synthesizeKey(name, modifiers); -} diff --git a/browser/devtools/framework/toolbox-options.js b/browser/devtools/framework/toolbox-options.js deleted file mode 100644 index fc452552998..00000000000 --- a/browser/devtools/framework/toolbox-options.js +++ /dev/null @@ -1,105 +0,0 @@ - -const { utils: Cu } = Components; -const DISABLED_TOOLS = "devtools.toolbox.disabledTools"; -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource:///modules/devtools/gDevTools.jsm"); - -window.addEventListener("load", function onLoad() { - window.removeEventListener("load", onLoad); - setupToolsList(); - populatePreferences(); -}); - -function setupToolsList() { - let disabledTools = []; - try { - disabledTools = JSON.parse(Services.prefs.getCharPref(DISABLED_TOOLS)); - } catch(ex) { - Cu.reportError("Error parsing pref " + DISABLED_TOOLS + " as JSON."); - } - let defaultToolsBox = document.getElementById("default-tools-box"); - let additionalToolsBox = document.getElementById("additional-tools-box"); - - defaultToolsBox.textContent = ""; - additionalToolsBox.textContent = ""; - - let onCheckboxClick = function(id) { - if (disabledTools.indexOf(id) > -1) { - disabledTools.splice(disabledTools.indexOf(id), 1); - Services.prefs.setCharPref(DISABLED_TOOLS, JSON.stringify(disabledTools)); - gDevTools.emit("tool-registered", id); - } - else { - disabledTools.push(id); - Services.prefs.setCharPref(DISABLED_TOOLS, JSON.stringify(disabledTools)); - gDevTools.emit("tool-unregistered", id); - } - }; - - // Populating the default tools lists - for (let tool of gDevTools.getDefaultTools()) { - let checkbox = document.createElement("checkbox"); - checkbox.setAttribute("id", tool.id); - checkbox.setAttribute("label", tool.label); - checkbox.setAttribute("tooltiptext", tool.tooltip || ""); - checkbox.setAttribute("checked", disabledTools.indexOf(tool.id) == -1); - checkbox.addEventListener("command", onCheckboxClick.bind(null, tool.id)); - defaultToolsBox.appendChild(checkbox); - } - - // Populating the additional tools list that came from add-ons. - let atleastOneAddon = false; - for (let tool of gDevTools.getAdditionalTools()) { - atleastOneAddon = true; - let checkbox = document.createElement("checkbox"); - checkbox.setAttribute("id", tool.id); - checkbox.setAttribute("label", tool.label); - checkbox.setAttribute("tooltiptext", tool.tooltip || ""); - checkbox.setAttribute("checked", disabledTools.indexOf(tool.id) == -1); - checkbox.addEventListener("command", onCheckboxClick.bind(null, tool.id)); - additionalToolsBox.appendChild(checkbox); - } - - if (!atleastOneAddon) { - additionalToolsBox.style.display = "none"; - additionalToolsBox.previousSibling.style.display = "none"; - } - - window.focus(); -} - -function populatePreferences() { - let prefCheckboxes = document.querySelectorAll("checkbox[data-pref]"); - for (let checkbox of prefCheckboxes) { - checkbox.checked = Services.prefs.getBoolPref(checkbox.getAttribute("data-pref")); - checkbox.addEventListener("command", function() { - let data = { - pref: this.getAttribute("data-pref"), - newValue: this.checked - }; - data.oldValue = Services.prefs.getBoolPref(data.pref); - Services.prefs.setBoolPref(data.pref, data.newValue); - gDevTools.emit("pref-changed", data); - }.bind(checkbox)); - } - let prefRadiogroups = document.querySelectorAll("radiogroup[data-pref]"); - for (let radiogroup of prefRadiogroups) { - let selectedValue = Services.prefs.getCharPref(radiogroup.getAttribute("data-pref")); - for (let radio of radiogroup.childNodes) { - radiogroup.selectedIndex = -1; - if (radio.getAttribute("value") == selectedValue) { - radiogroup.selectedItem = radio; - break; - } - } - radiogroup.addEventListener("select", function() { - let data = { - pref: this.getAttribute("data-pref"), - newValue: this.selectedItem.getAttribute("value") - }; - data.oldValue = Services.prefs.getCharPref(data.pref); - Services.prefs.setCharPref(data.pref, data.newValue); - gDevTools.emit("pref-changed", data); - }.bind(radiogroup)); - } -} diff --git a/browser/devtools/framework/toolbox-options.xul b/browser/devtools/framework/toolbox-options.xul deleted file mode 100644 index dbd4e835761..00000000000 --- a/browser/devtools/framework/toolbox-options.xul +++ /dev/null @@ -1,44 +0,0 @@ - - - - %toolboxDTD; -]> - - - - - - - - - - - - diff --git a/browser/devtools/framework/toolbox.css b/browser/devtools/framework/toolbox.css index efd054db508..03a48c9aaab 100644 --- a/browser/devtools/framework/toolbox.css +++ b/browser/devtools/framework/toolbox.css @@ -13,17 +13,3 @@ .command-button > .toolbarbutton-text { display: none; } - -#options-panel { - overflow-y: auto; - display: block; -} - -.options-vertical-pane { - display: inline; - float: left; -} - -.options-vertical-pane > label { - display: block; -} diff --git a/browser/devtools/framework/toolbox.xul b/browser/devtools/framework/toolbox.xul index 16c26961592..297e0c296ad 100644 --- a/browser/devtools/framework/toolbox.xul +++ b/browser/devtools/framework/toolbox.xul @@ -18,12 +18,6 @@ - - - @@ -34,10 +28,6 @@ #endif - @@ -51,14 +41,6 @@ #endif - - - diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn index 8c49610729e..dbed935bab4 100644 --- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -57,8 +57,6 @@ browser.jar: content/browser/devtools/commandlineoutput.xhtml (commandline/commandlineoutput.xhtml) content/browser/devtools/commandlinetooltip.xhtml (commandline/commandlinetooltip.xhtml) content/browser/devtools/framework/toolbox-window.xul (framework/toolbox-window.xul) - content/browser/devtools/framework/toolbox-options.xul (framework/toolbox-options.xul) - content/browser/devtools/framework/toolbox-options.js (framework/toolbox-options.js) * content/browser/devtools/framework/toolbox.xul (framework/toolbox.xul) content/browser/devtools/framework/toolbox.css (framework/toolbox.css) content/browser/devtools/inspector/inspector.xul (inspector/inspector.xul) diff --git a/browser/devtools/shared/theme-switching.js b/browser/devtools/shared/theme-switching.js index ce25a821790..044ae9bde16 100644 --- a/browser/devtools/shared/theme-switching.js +++ b/browser/devtools/shared/theme-switching.js @@ -3,65 +3,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ (function() { - const DEVTOOLS_SKIN_URL = "chrome://browser/skin/devtools/"; - - function forceStyle() { - let computedStyle = window.getComputedStyle(document.documentElement); - if (!computedStyle) { - // Null when documentElement is not ready. This method is anyways not - // required then as scrollbars would be in their state without flushing. - return; - } - let display = computedStyle.display; // Save display value - document.documentElement.style.display = "none"; - window.getComputedStyle(document.documentElement).display; // Flush - document.documentElement.style.display = display; // Restore - } - - function switchTheme(theme, oldTheme) { - let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - if (oldTheme && theme != oldTheme) { - let old_theme_url = Services.io.newURI(DEVTOOLS_SKIN_URL + oldTheme + - "-theme.css", null, null); - try { - winUtils.removeSheet(old_theme_url, window.AUTHOR_SHEET); - } catch(ex) {} - } - let theme_url = Services.io.newURI( + - theme + "-theme.css", null, null); - winUtils.loadSheet(theme_url, window.AUTHOR_SHEET); - let scrollbar_url = - Services.io.newURI(DEVTOOLS_SKIN_URL + "floating-scrollbars-light.css", - null, null); - if (theme == "dark") { - winUtils.loadSheet(scrollbar_url, window.AGENT_SHEET); - forceStyle(); - } - else if (oldTheme == "dark") { - try { - winUtils.removeSheet(scrollbar_url, window.AGENT_SHEET); - } catch(ex) {} - forceStyle(); - } - document.documentElement.classList.remove("theme-" + oldTheme); - document.documentElement.classList.add("theme-" + theme); - } - - function handlePrefChange(event, data) { - switch(data.pref) { - case "devtools.theme": - switchTheme(data.newValue, data.oldValue); - } - } - const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/Services.jsm"); - Cu.import("resource:///modules/devtools/gDevTools.jsm"); let theme = Services.prefs.getCharPref("devtools.theme"); - switchTheme(theme); - gDevTools.on("pref-changed", handlePrefChange); - window.addEventListener("unload", function() { - gDevTools.off("pref-changed", handlePrefChange); - }); + let theme_url = Services.io.newURI("chrome://browser/skin/devtools/" + theme + "-theme.css", null, null); + let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + winUtils.loadSheet(theme_url, window.AUTHOR_SHEET); + if (theme == "dark") { + let scrollbar_url = Services.io.newURI("chrome://browser/skin/devtools/floating-scrollbars-light.css", null, null); + winUtils.loadSheet(scrollbar_url, window.AGENT_SHEET); + } + document.documentElement.classList.add("theme-" + theme); })() diff --git a/browser/devtools/shared/widgets/SideMenuWidget.jsm b/browser/devtools/shared/widgets/SideMenuWidget.jsm index 5e98ee6a88a..d11c898f23b 100644 --- a/browser/devtools/shared/widgets/SideMenuWidget.jsm +++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm @@ -117,9 +117,7 @@ SideMenuWidget.prototype = { * The element associated with the displayed item. */ removeChild: function SMW_removeChild(aChild) { - // Remove the item itself, not the contents. - let item = aChild.parentNode; - item.parentNode.removeChild(item); + aChild.parentNode.removeChild(aChild); this._orderedMenuElementsArray.splice( this._orderedMenuElementsArray.indexOf(aChild), 1); diff --git a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd index 86339ba8713..57efe9e36df 100644 --- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd @@ -4,19 +4,4 @@ - - - - - - - - - - - - - - - - + diff --git a/browser/themes/linux/devtools/toolbox.css b/browser/themes/linux/devtools/toolbox.css index 42807bb0aef..ed42d821296 100644 --- a/browser/themes/linux/devtools/toolbox.css +++ b/browser/themes/linux/devtools/toolbox.css @@ -128,19 +128,6 @@ -moz-image-region: rect(0px, 48px, 16px, 32px); } -#toolbox-tab-options { - list-style-image: url("chrome://browser/skin/devtools/option-icon.png"); - -moz-image-region: rect(0px 16px 16px 0px); -} - -#toolbox-tab-options[checked=true] { - -moz-image-region: rect(0px 32px 16px 16px); -} - -#toolbox-tab-options > image { - opacity: 1; - -moz-margin-start: 0; -} /* Tabs */ @@ -237,28 +224,3 @@ background-position: top right, top left, left, right; box-shadow: 1px -1px 0 hsla(206,37%,4%,.2) inset; } - -#options-panel { - background-image: url("chrome://browser/skin/newtab/noise.png"); -} - -.options-vertical-pane { - margin: 15px; - width: calc(50% - 30px); - min-width: 400px; - -moz-padding-start: 5px; -} - -.options-vertical-pane > label { - padding: 5px 0; - font-size: 1.4rem; -} - -.options-groupbox { - -moz-margin-start: 15px; - padding: 4px; -} - -.options-groupbox > * { - padding: 2px; -} diff --git a/browser/themes/osx/devtools/toolbox.css b/browser/themes/osx/devtools/toolbox.css index 4866fcc4592..cca78b2264b 100644 --- a/browser/themes/osx/devtools/toolbox.css +++ b/browser/themes/osx/devtools/toolbox.css @@ -116,19 +116,6 @@ -moz-image-region: rect(0px, 48px, 16px, 32px); } -#toolbox-tab-options { - list-style-image: url("chrome://browser/skin/devtools/option-icon.png"); - -moz-image-region: rect(0px 16px 16px 0px); -} - -#toolbox-tab-options[checked=true] { - -moz-image-region: rect(0px 32px 16px 16px); -} - -#toolbox-tab-options > image { - opacity: 1; - -moz-margin-start: 0; -} /* Tabs */ @@ -224,28 +211,3 @@ background-position: top right, top left, left, right; box-shadow: 1px -1px 0 hsla(206,37%,4%,.2) inset; } - -#options-panel { - background-image: url("chrome://browser/skin/newtab/noise.png"); -} - -.options-vertical-pane { - margin: 15px; - width: calc(50% - 30px); - min-width: 400px; - -moz-padding-start: 5px; -} - -.options-vertical-pane > label { - padding: 5px 0; - font-size: 1.4rem; -} - -.options-groupbox { - -moz-margin-start: 15px; - padding: 4px; -} - -.options-groupbox > * { - padding: 2px; -} diff --git a/browser/themes/windows/devtools/toolbox.css b/browser/themes/windows/devtools/toolbox.css index 4540653bf55..2361d5f196e 100644 --- a/browser/themes/windows/devtools/toolbox.css +++ b/browser/themes/windows/devtools/toolbox.css @@ -131,19 +131,6 @@ -moz-image-region: rect(0px, 48px, 16px, 32px); } -#toolbox-tab-options { - list-style-image: url("chrome://browser/skin/devtools/option-icon.png"); - -moz-image-region: rect(0px 16px 16px 0px); -} - -#toolbox-tab-options[checked=true] { - -moz-image-region: rect(0px 32px 16px 16px); -} - -#toolbox-tab-options > image { - opacity: 1; - -moz-margin-start: 0; -} /* Tabs */ @@ -233,28 +220,3 @@ background-position: top right, top left, left, right; box-shadow: 1px -1px 0 hsla(206,37%,4%,.2) inset; } - -#options-panel { - background-image: url("chrome://browser/skin/newtab/noise.png"); -} - -.options-vertical-pane { - margin: 15px; - width: calc(50% - 30px); - min-width: 400px; - -moz-padding-start: 5px; -} - -.options-vertical-pane > label { - padding: 5px 0; - font-size: 1.4rem; -} - -.options-groupbox { - -moz-margin-start: 15px; - padding: 4px; -} - -.options-groupbox > * { - padding: 2px; -} From 97c007a0fb76e66bff8c151c7ad5e704b17fd40f Mon Sep 17 00:00:00 2001 From: Mihai Sucan Date: Mon, 15 Apr 2013 19:10:04 +0300 Subject: [PATCH 05/14] Bug 851231 - Output Console.jsm API calls to the browser console; r=jwalker --- browser/devtools/webconsole/test/Makefile.in | 1 + .../test/browser_console_consolejsm_output.js | 118 +++++++++ .../test/browser_longstring_hang.js | 19 +- browser/devtools/webconsole/test/head.js | 241 ++++++++++++++++-- toolkit/devtools/Console.jsm | 170 ++++++++++-- 5 files changed, 490 insertions(+), 59 deletions(-) create mode 100644 browser/devtools/webconsole/test/browser_console_consolejsm_output.js diff --git a/browser/devtools/webconsole/test/Makefile.in b/browser/devtools/webconsole/test/Makefile.in index bea96298e45..caa8fa267f0 100644 --- a/browser/devtools/webconsole/test/Makefile.in +++ b/browser/devtools/webconsole/test/Makefile.in @@ -121,6 +121,7 @@ MOCHITEST_BROWSER_FILES = \ browser_console_variables_view_while_debugging.js \ browser_console.js \ browser_longstring_hang.js \ + browser_console_consolejsm_output.js \ head.js \ $(NULL) diff --git a/browser/devtools/webconsole/test/browser_console_consolejsm_output.js b/browser/devtools/webconsole/test/browser_console_consolejsm_output.js new file mode 100644 index 00000000000..9bd8b10c192 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_consolejsm_output.js @@ -0,0 +1,118 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that Console.jsm outputs messages to the Browser Console, bug 851231. + +function test() +{ + HUDConsoleUI.toggleBrowserConsole().then(consoleOpened); + let hud = null; + + function consoleOpened(aHud) + { + hud = aHud; + hud.jsterm.clearOutput(true); + + let console = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).console; + + console.time("foobarTimer"); + let foobar = { bug851231prop: "bug851231value" }; + + console.log("bug851231-log"); + console.info("bug851231-info"); + console.warn("bug851231-warn"); + console.error("bug851231-error", foobar); + console.debug("bug851231-debug"); + console.trace(); + console.dir(document); + console.timeEnd("foobarTimer"); + + info("wait for the Console.jsm messages"); + + waitForMessages({ + webconsole: hud, + messages: [ + { + name: "console.log output", + text: "bug851231-log", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + name: "console.info output", + text: "bug851231-info", + category: CATEGORY_WEBDEV, + severity: SEVERITY_INFO, + }, + { + name: "console.warn output", + text: "bug851231-warn", + category: CATEGORY_WEBDEV, + severity: SEVERITY_WARNING, + }, + { + name: "console.error output", + text: /\bbug851231-error\b.+\[object Object\]/, + category: CATEGORY_WEBDEV, + severity: SEVERITY_ERROR, + objects: true, + }, + { + name: "console.debug output", + text: "bug851231-debug", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + name: "console.trace output", + consoleTrace: { + file: "browser_console_consolejsm_output.js", + fn: "consoleOpened", + }, + }, + { + name: "console.dir output", + consoleDir: "[object XULDocument]", + }, + { + name: "console.time output", + consoleTime: "foobarTimer", + }, + { + name: "console.timeEnd output", + consoleTimeEnd: "foobarTimer", + }, + ], + }).then((aResults) => { + let consoleErrorMsg = aResults[3]; + ok(consoleErrorMsg, "console.error message element found"); + let clickable = consoleErrorMsg.clickableElements[0]; + ok(clickable, "clickable object found for console.error"); + + let onFetch = (aEvent, aVar) => { + // Skip the notification from console.dir variablesview-fetched. + if (aVar._variablesView != hud.jsterm._variablesView) { + return; + } + hud.jsterm.off("variablesview-fetched", onFetch); + + ok(aVar, "object inspector opened on click"); + + findVariableViewProperties(aVar, [{ + name: "bug851231prop", + value: "bug851231value", + }], { webconsole: hud }).then(finishTest); + }; + + hud.jsterm.on("variablesview-fetched", onFetch); + + scrollOutputToNode(clickable); + + info("wait for variablesview-fetched"); + executeSoon(() => + EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow)); + }); + } +} diff --git a/browser/devtools/webconsole/test/browser_longstring_hang.js b/browser/devtools/webconsole/test/browser_longstring_hang.js index 4ec5b2070be..0a36006801e 100644 --- a/browser/devtools/webconsole/test/browser_longstring_hang.js +++ b/browser/devtools/webconsole/test/browser_longstring_hang.js @@ -42,13 +42,10 @@ function test() function onInitialString(aResults) { - let msg = [...aResults[0].matched][0]; - ok(msg, "console.log result message element"); - - let clickable = msg.querySelector(".longStringEllipsis"); + let clickable = aResults[0].longStrings[0]; ok(clickable, "long string ellipsis is shown"); - scrollToVisible(clickable); + scrollOutputToNode(clickable); executeSoon(() => { EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow); @@ -73,16 +70,4 @@ function test() }).then(finishTest); }); } - - function scrollToVisible(aNode) - { - let richListBoxNode = aNode.parentNode; - while (richListBoxNode.tagName != "richlistbox") { - richListBoxNode = richListBoxNode.parentNode; - } - - let boxObject = richListBoxNode.scrollBoxObject; - let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject); - nsIScrollBoxObject.ensureElementIsVisible(aNode); - } } diff --git a/browser/devtools/webconsole/test/head.js b/browser/devtools/webconsole/test/head.js index 97e84c9a0cb..4f15d19d4a5 100644 --- a/browser/devtools/webconsole/test/head.js +++ b/browser/devtools/webconsole/test/head.js @@ -252,7 +252,7 @@ function waitForOpenContextMenu(aContextMenu, aOptions) { function dumpConsoles() { if (gPendingOutputTest) { - console.log("dumpConsoles"); + console.log("dumpConsoles start"); for each (let hud in HUDService.hudReferences) { if (!hud.outputNode) { console.debug("no output content for", hud.hudId); @@ -261,25 +261,38 @@ function dumpConsoles() console.debug("output content for", hud.hudId); for (let elem of hud.outputNode.childNodes) { - let text = getMessageElementText(elem); - let repeats = elem.querySelector(".webconsole-msg-repeat"); - if (repeats) { - repeats = repeats.getAttribute("value"); - } - console.debug("date", elem.timestamp, - "class", elem.className, - "category", elem.category, - "severity", elem.severity, - "repeats", repeats, - "clipboardText", elem.clipboardText, - "text", text); + dumpMessageElement(elem); } } + console.log("dumpConsoles end"); gPendingOutputTest = 0; } } +/** + * Dump to output debug information for the given webconsole message. + * + * @param nsIDOMNode aMessage + * The message element you want to display. + */ +function dumpMessageElement(aMessage) +{ + let text = getMessageElementText(aMessage); + let repeats = aMessage.querySelector(".webconsole-msg-repeat"); + if (repeats) { + repeats = repeats.getAttribute("value"); + } + console.debug("id", aMessage.getAttribute("id"), + "date", aMessage.timestamp, + "class", aMessage.className, + "category", aMessage.category, + "severity", aMessage.severity, + "repeats", repeats, + "clipboardText", aMessage.clipboardText, + "text", text); +} + function finishTest() { browser = hudId = hud = filterBox = outputNode = cs = null; @@ -888,6 +901,104 @@ function waitForMessages(aOptions) return result; } + function checkConsoleTrace(aRule, aElement) + { + let elemText = getMessageElementText(aElement); + let trace = aRule.consoleTrace; + + if (!checkText("Stack trace from ", elemText)) { + return false; + } + + let clickable = aElement.querySelector(".hud-clickable"); + if (!clickable) { + ok(false, "console.trace() message is missing .hud-clickable"); + displayErrorContext(aRule, aElement); + return false; + } + aRule.clickableElements = [clickable]; + + if (trace.file && + !checkText("from " + trace.file + ", ", elemText)) { + ok(false, "console.trace() message is missing the file name: " + + trace.file); + displayErrorContext(aRule, aElement); + return false; + } + + if (trace.fn && + !checkText(", function " + trace.fn + ", ", elemText)) { + ok(false, "console.trace() message is missing the function name: " + + trace.fn); + displayErrorContext(aRule, aElement); + return false; + } + + if (trace.line && + !checkText(", line " + trace.line + ".", elemText)) { + ok(false, "console.trace() message is missing the line number: " + + trace.line); + displayErrorContext(aRule, aElement); + return false; + } + + aRule.category = CATEGORY_WEBDEV; + aRule.severity = SEVERITY_LOG; + + return true; + } + + function checkConsoleTime(aRule, aElement) + { + let elemText = getMessageElementText(aElement); + let time = aRule.consoleTime; + + if (!checkText(time + ": timer started", elemText)) { + return false; + } + + aRule.category = CATEGORY_WEBDEV; + aRule.severity = SEVERITY_LOG; + + return true; + } + + function checkConsoleTimeEnd(aRule, aElement) + { + let elemText = getMessageElementText(aElement); + let time = aRule.consoleTimeEnd; + let regex = new RegExp(time + ": \\d+ms"); + + if (!checkText(regex, elemText)) { + return false; + } + + aRule.category = CATEGORY_WEBDEV; + aRule.severity = SEVERITY_LOG; + + return true; + } + + function checkConsoleDir(aRule, aElement) + { + if (!aElement.classList.contains("webconsole-msg-inspector")) { + return false; + } + + let elemText = getMessageElementText(aElement); + if (!checkText(aRule.consoleDir, elemText)) { + return false; + } + + let iframe = aElement.querySelector("iframe"); + if (!iframe) { + ok(false, "console.dir message has no iframe"); + return false; + } + + return true; + } + function checkMessage(aRule, aElement) { let elemText = getMessageElementText(aElement); @@ -900,16 +1011,41 @@ function waitForMessages(aOptions) return false; } - if (aRule.category) { - if (aElement.category != aRule.category) { - return false; - } + if (aRule.consoleTrace && !checkConsoleTrace(aRule, aElement)) { + return false; } - if (aRule.severity) { - if (aElement.severity != aRule.severity) { - return false; + if (aRule.consoleTime && !checkConsoleTime(aRule, aElement)) { + return false; + } + + if (aRule.consoleTimeEnd && !checkConsoleTimeEnd(aRule, aElement)) { + return false; + } + + if (aRule.consoleDir && !checkConsoleDir(aRule, aElement)) { + return false; + } + + let partialMatch = !!(aRule.consoleTrace || aRule.consoleTime || + aRule.consoleTimeEnd); + + if (aRule.category && aElement.category != aRule.category) { + if (partialMatch) { + is(aElement.category, aRule.category, + "message category for rule: " + displayRule(aRule)); + displayErrorContext(aRule, aElement); } + return false; + } + + if (aRule.severity && aElement.severity != aRule.severity) { + if (partialMatch) { + is(aElement.severity, aRule.severity, + "message severity for rule: " + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; } if (aRule.repeats) { @@ -919,9 +1055,32 @@ function waitForMessages(aOptions) } } - let longString = !!aElement.querySelector(".longStringEllipsis"); - if ("longString" in aRule && aRule.longString != longString) { - return false; + if ("longString" in aRule) { + let longStrings = aElement.querySelectorAll(".longStringEllipsis"); + if (aRule.longString != !!longStrings[0]) { + if (partialMatch) { + is(!!longStrings[0], aRule.longString, + "long string existence check failed for message rule: " + + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + aRule.longStrings = longStrings; + } + + if ("objects" in aRule) { + let clickables = aElement.querySelectorAll(".hud-clickable"); + if (aRule.objects != !!clickables[0]) { + if (partialMatch) { + is(!!clickables[0], aRule.objects, + "objects existence check failed for message rule: " + + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + aRule.clickableElements = clickables; } let count = aRule.count || 1; @@ -936,6 +1095,16 @@ function waitForMessages(aOptions) function onMessagesAdded(aEvent, aNewElements) { for (let elem of aNewElements) { + let location = elem.querySelector(".webconsole-location"); + if (location) { + let url = location.getAttribute("title"); + // Prevent recursion with the browser console and any potential + // messages coming from head.js. + if (url.indexOf("browser/devtools/webconsole/test/head.js") != -1) { + continue; + } + } + for (let rule of rules) { if (rule._ruleMatched) { continue; @@ -989,6 +1158,13 @@ function waitForMessages(aOptions) return aRule.name || aRule.text; } + function displayErrorContext(aRule, aElement) + { + console.log("error occured during rule " + displayRule(aRule)); + console.log("while checking the following message"); + dumpMessageElement(aElement); + } + executeSoon(() => { onMessagesAdded("messages-added", webconsole.outputNode.childNodes); if (rulesMatched != rules.length) { @@ -1001,3 +1177,22 @@ function waitForMessages(aOptions) return deferred.promise; } + + +/** + * Scroll the Web Console output to the given node. + * + * @param nsIDOMNode aNode + * The node to scroll to. + */ +function scrollOutputToNode(aNode) +{ + let richListBoxNode = aNode.parentNode; + while (richListBoxNode.tagName != "richlistbox") { + richListBoxNode = richListBoxNode.parentNode; + } + + let boxObject = richListBoxNode.scrollBoxObject; + let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject); + nsIScrollBoxObject.ensureElementIsVisible(aNode); +} diff --git a/toolkit/devtools/Console.jsm b/toolkit/devtools/Console.jsm index 8d78dfabd21..6a90d079ceb 100644 --- a/toolkit/devtools/Console.jsm +++ b/toolkit/devtools/Console.jsm @@ -24,6 +24,11 @@ this.EXPORTED_SYMBOLS = [ "console" ]; Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +let gTimerRegistry = new Map(); + /** * String utility to ensure that strings are a specified length. Strings * that are too long are truncated to the max length and the last char is @@ -250,7 +255,7 @@ function logProperty(aProp, aValue) { /** * Parse a stack trace, returning an array of stack frame objects, where - * each has file/line/call members + * each has filename/lineNumber/functionName members * * @param {string} aStack * The serialized stack trace @@ -266,35 +271,42 @@ function parseStack(aStack) { let at = line.lastIndexOf("@"); let posn = line.substring(at + 1); trace.push({ - file: posn.split(":")[0], - line: posn.split(":")[1], - call: line.substring(0, at) + filename: posn.split(":")[0], + lineNumber: posn.split(":")[1], + functionName: line.substring(0, at) }); }); return trace; } /** - * parseStack() takes output from an exception from which it creates the an - * array of stack frame objects, this has the same output but using data from - * Components.stack + * Format a frame coming from Components.stack such that it can be used by the + * Browser Console, via console-api-log-event notifications. * - * @param {string} aFrame - * The stack frame from which to begin the walk + * @param {object} aFrame + * The stack frame from which to begin the walk. + * @param {number=0} aMaxDepth + * Maximum stack trace depth. Default is 0 - no depth limit. * @return {object[]} - * Array of { file: "...", line: NNN, call: "..." } objects + * An array of {filename, lineNumber, functionName, language} objects. + * These objects follow the same format as other console-api-log-event + * messages. */ -function getStack(aFrame) { +function getStack(aFrame, aMaxDepth = 0) { if (!aFrame) { aFrame = Components.stack.caller; } let trace = []; while (aFrame) { trace.push({ - file: aFrame.filename, - line: aFrame.lineNumber, - call: aFrame.name + filename: aFrame.filename, + lineNumber: aFrame.lineNumber, + functionName: aFrame.name, + language: aFrame.language, }); + if (aMaxDepth == trace.length) { + break; + } aFrame = aFrame.caller; } return trace; @@ -311,13 +323,52 @@ function getStack(aFrame) { function formatTrace(aTrace) { let reply = ""; aTrace.forEach(function(frame) { - reply += fmt(frame.file, 20, 20, { truncate: "start" }) + " " + - fmt(frame.line, 5, 5) + " " + - fmt(frame.call, 75, 75) + "\n"; + reply += fmt(frame.filename, 20, 20, { truncate: "start" }) + " " + + fmt(frame.lineNumber, 5, 5) + " " + + fmt(frame.functionName, 75, 75) + "\n"; }); return reply; } +/** + * Create a new timer by recording the current time under the specified name. + * + * @param {string} aName + * The name of the timer. + * @param {number} [aTimestamp=Date.now()] + * Optional timestamp that tells when the timer was originally started. + * @return {object} + * The name property holds the timer name and the started property + * holds the time the timer was started. In case of error, it returns + * an object with the single property "error" that contains the key + * for retrieving the localized error message. + */ +function startTimer(aName, aTimestamp) { + let key = aName.toString(); + if (!gTimerRegistry.has(key)) { + gTimerRegistry.set(key, aTimestamp || Date.now()); + } + return { name: aName, started: gTimerRegistry.get(key) }; +} + +/** + * Stop the timer with the specified name and retrieve the elapsed time. + * + * @param {string} aName + * The name of the timer. + * @param {number} [aTimestamp=Date.now()] + * Optional timestamp that tells when the timer was originally stopped. + * @return {object} + * The name property holds the timer name and the duration property + * holds the number of milliseconds since the timer was started. + */ +function stopTimer(aName, aTimestamp) { + let key = aName.toString(); + let duration = (aTimestamp || Date.now()) - gTimerRegistry.get(key); + gTimerRegistry.delete(key); + return { name: aName, duration: duration }; +} + /** * Create a function which will output a concise level of output when used * as a logging function @@ -332,6 +383,8 @@ function formatTrace(aTrace) { function createDumper(aLevel) { return function() { let args = Array.prototype.slice.call(arguments, 0); + let frame = getStack(Components.stack.caller, 1)[0]; + sendConsoleAPIMessage(aLevel, frame, args); let data = args.map(function(arg) { return stringify(arg); }); @@ -354,12 +407,72 @@ function createMultiLineDumper(aLevel) { return function() { dump("console." + aLevel + ": \n"); let args = Array.prototype.slice.call(arguments, 0); + let frame = getStack(Components.stack.caller, 1)[0]; + sendConsoleAPIMessage(aLevel, frame, args); args.forEach(function(arg) { dump(log(arg)); }); }; } +/** + * Send a Console API message. This function will send a console-api-log-event + * notification through the nsIObserverService. + * + * @param {string} aLevel + * Message severity level. This is usually the name of the console method + * that was called. + * @param {object} aFrame + * The youngest stack frame coming from Components.stack, as formatted by + * getStack(). + * @param {array} aArgs + * The arguments given to the console method. + * @param {object} aOptions + * Object properties depend on the console method that was invoked: + * - timer: for time() and timeEnd(). Holds the timer information. + * - groupName: for group(), groupCollapsed() and groupEnd(). + * - stacktrace: for trace(). Holds the array of stack frames as given by + * getStack(). + */ +function sendConsoleAPIMessage(aLevel, aFrame, aArgs, aOptions = {}) +{ + let consoleEvent = { + ID: aFrame.filename, + level: aLevel, + filename: aFrame.filename, + lineNumber: aFrame.lineNumber, + functionName: aFrame.functionName, + timeStamp: Date.now(), + arguments: aArgs, + }; + + consoleEvent.wrappedJSObject = consoleEvent; + + switch (aLevel) { + case "trace": + consoleEvent.stacktrace = aOptions.stacktrace; + break; + case "time": + case "timeEnd": + consoleEvent.timer = aOptions.timer; + break; + case "group": + case "groupCollapsed": + case "groupEnd": + try { + consoleEvent.groupName = Array.prototype.join.call(aArgs, " "); + } + catch (ex) { + Cu.reportError(ex); + Cu.reportError(ex.stack); + return; + } + break; + } + + Services.obs.notifyObservers(consoleEvent, "console-api-log-event", null); +} + /** * This creates a console object that somewhat replicates Firebug's console * object. It currently writes to dump(), but should write to the web @@ -373,13 +486,32 @@ this.console = { error: createMultiLineDumper("error"), trace: function Console_trace() { + let args = Array.prototype.slice.call(arguments, 0); let trace = getStack(Components.stack.caller); - dump(formatTrace(trace) + "\n"); + sendConsoleAPIMessage("trace", trace[0], args, + { stacktrace: trace }); + dump("console.trace:\n" + formatTrace(trace) + "\n"); }, clear: function Console_clear() {}, dir: createMultiLineDumper("dir"), dirxml: createMultiLineDumper("dirxml"), group: createDumper("group"), - groupEnd: createDumper("groupEnd") + groupEnd: createDumper("groupEnd"), + + time: function Console_time() { + let args = Array.prototype.slice.call(arguments, 0); + let frame = getStack(Components.stack.caller, 1)[0]; + let timer = startTimer(args[0]); + sendConsoleAPIMessage("time", frame, args, { timer: timer }); + dump("console.time: '" + timer.name + "' @ " + (new Date()) + "\n"); + }, + + timeEnd: function Console_timeEnd() { + let args = Array.prototype.slice.call(arguments, 0); + let frame = getStack(Components.stack.caller, 1)[0]; + let timer = stopTimer(args[0]); + sendConsoleAPIMessage("timeEnd", frame, args, { timer: timer }); + dump("console.timeEnd: '" + timer.name + "' " + timer.duration + "ms\n"); + }, }; From 4cb58bb08d853b00c331500c45188ce11493dae5 Mon Sep 17 00:00:00 2001 From: Panos Astithas Date: Tue, 16 Apr 2013 08:52:55 +0300 Subject: [PATCH 06/14] Bug 861852 - Telemetry data from the remote protocol always appear in the remote buckets; r=rcampbell --- toolkit/devtools/debugger/dbg-client.jsm | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/toolkit/devtools/debugger/dbg-client.jsm b/toolkit/devtools/debugger/dbg-client.jsm index f1c037aa3af..bd1db6e50a7 100644 --- a/toolkit/devtools/debugger/dbg-client.jsm +++ b/toolkit/devtools/debugger/dbg-client.jsm @@ -245,7 +245,7 @@ DebuggerClient.requester = function DC_requester(aPacketSkeleton, { telemetry, return function (...args) { let histogram, startTime; if (telemetry) { - let transportType = this._transport instanceof LocalDebuggerTransport + let transportType = this._transport.onOutputStreamReady === undefined ? "LOCAL_" : "REMOTE_"; let histogramId = "DEVTOOLS_DEBUGGER_RDP_" @@ -253,7 +253,6 @@ DebuggerClient.requester = function DC_requester(aPacketSkeleton, { telemetry, histogram = Services.telemetry.getHistogramById(histogramId); startTime = +new Date; } - let outgoingPacket = { to: aPacketSkeleton.to || this.actor }; @@ -831,6 +830,7 @@ function TabClient(aClient, aActor) { TabClient.prototype = { get actor() { return this._actor }, + get _transport() { return this._client._transport; }, /** * Detach the client from the tab actor. @@ -885,6 +885,7 @@ ThreadClient.prototype = { get actor() { return this._actor; }, get compat() { return this._client.compat; }, + get _transport() { return this._client._transport; }, _assertPaused: function TC_assertPaused(aCommand) { if (!this.paused) { @@ -1410,6 +1411,7 @@ function GripClient(aClient, aGrip) GripClient.prototype = { get actor() { return this._grip.actor }, + get _transport() { return this._client._transport; }, valid: true, @@ -1500,6 +1502,7 @@ LongStringClient.prototype = { get actor() { return this._grip.actor; }, get length() { return this._grip.length; }, get initial() { return this._grip.initial; }, + get _transport() { return this._client._transport; }, valid: true, @@ -1536,6 +1539,8 @@ function SourceClient(aClient, aForm) { } SourceClient.prototype = { + get _transport() { return this._client._transport; }, + /** * Get a long string grip for this SourceClient's source. */ @@ -1593,6 +1598,7 @@ BreakpointClient.prototype = { _actor: null, get actor() { return this._actor; }, + get _transport() { return this._client._transport; }, /** * Remove the breakpoint from the server. From fd19397aa42566be6e3b7077750405c6bf4b4355 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Tue, 16 Apr 2013 00:07:00 +0300 Subject: [PATCH 07/14] Bug 772119 - expose source mapped sources over the remote debugging protocol; r=past --- browser/app/profile/firefox.js | 1 + .../devtools/debugger/debugger-controller.js | 4 +- browser/devtools/debugger/test/Makefile.in | 5 + .../debugger/test/binary_search.coffee | 18 + .../devtools/debugger/test/binary_search.html | 12 + .../devtools/debugger/test/binary_search.js | 30 + .../devtools/debugger/test/binary_search.map | 9 + ...rowser_dbg_bug731394_editor-contextmenu.js | 19 +- .../test/browser_dbg_source_maps-01.js | 156 ++++ toolkit/devtools/debugger/dbg-client.jsm | 104 +-- .../debugger/server/dbg-script-actors.js | 678 ++++++++++++------ .../devtools/debugger/server/dbg-server.js | 53 +- .../devtools/debugger/tests/unit/head_dbg.js | 4 +- .../debugger/tests/unit/test_breakpoint-04.js | 4 +- .../debugger/tests/unit/test_dbgactor.js | 1 + .../tests/unit/test_listsources-02.js | 2 +- .../tests/unit/test_listsources-03.js | 52 ++ .../tests/unit/test_profiler_actor.js | 20 +- .../debugger/tests/unit/test_sourcemaps-01.js | 64 ++ .../debugger/tests/unit/test_sourcemaps-02.js | 73 ++ .../debugger/tests/unit/test_sourcemaps-03.js | 149 ++++ .../debugger/tests/unit/testcompatactors.js | 38 +- .../devtools/debugger/tests/unit/xpcshell.ini | 4 + toolkit/devtools/sourcemap/SourceMap.jsm | 476 ++++++++++-- .../devtools/sourcemap/tests/unit/Utils.jsm | 133 +++- .../tests/unit/test_source_map_consumer.js | 111 +++ .../tests/unit/test_source_map_generator.js | 106 ++- .../sourcemap/tests/unit/test_source_node.js | 121 +++- 28 files changed, 2046 insertions(+), 401 deletions(-) create mode 100644 browser/devtools/debugger/test/binary_search.coffee create mode 100644 browser/devtools/debugger/test/binary_search.html create mode 100644 browser/devtools/debugger/test/binary_search.js create mode 100644 browser/devtools/debugger/test/binary_search.map create mode 100644 browser/devtools/debugger/test/browser_dbg_source_maps-01.js create mode 100644 toolkit/devtools/debugger/tests/unit/test_listsources-03.js create mode 100644 toolkit/devtools/debugger/tests/unit/test_sourcemaps-01.js create mode 100644 toolkit/devtools/debugger/tests/unit/test_sourcemaps-02.js create mode 100644 toolkit/devtools/debugger/tests/unit/test_sourcemaps-03.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 3dc3ef22751..90dac6abad6 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1060,6 +1060,7 @@ pref("devtools.debugger.remote-host", "localhost"); pref("devtools.debugger.remote-autoconnect", false); pref("devtools.debugger.remote-connection-retries", 3); pref("devtools.debugger.remote-timeout", 20000); +pref("devtools.debugger.source-maps-enabled", false); // The default Debugger UI settings pref("devtools.debugger.ui.win-x", 0); diff --git a/browser/devtools/debugger/debugger-controller.js b/browser/devtools/debugger/debugger-controller.js index fc2f51a5087..9df4b43e126 100644 --- a/browser/devtools/debugger/debugger-controller.js +++ b/browser/devtools/debugger/debugger-controller.js @@ -253,6 +253,8 @@ let DebuggerController = { if (aCallback) { aCallback(); } + }, { + useSourceMaps: Services.prefs.getBoolPref("devtools.debugger.source-maps-enabled") }); }, @@ -1102,7 +1104,7 @@ SourceScripts.prototype = { */ _onSourcesAdded: function SS__onSourcesAdded(aResponse) { if (aResponse.error) { - Cu.reportError("Error getting sources: " + aResponse.message); + Cu.reportError(new Error("Error getting sources: " + aResponse.message)); return; } diff --git a/browser/devtools/debugger/test/Makefile.in b/browser/devtools/debugger/test/Makefile.in index 7f01762ad84..07eeed6da26 100644 --- a/browser/devtools/debugger/test/Makefile.in +++ b/browser/devtools/debugger/test/Makefile.in @@ -96,6 +96,7 @@ MOCHITEST_BROWSER_TESTS = \ browser_dbg_bfcache.js \ browser_dbg_progress-listener-bug.js \ browser_dbg_chrome-debugging.js \ + browser_dbg_source_maps-01.js \ head.js \ helpers.js \ $(NULL) @@ -127,6 +128,10 @@ MOCHITEST_BROWSER_PAGES = \ test-function-search-01.js \ test-function-search-02.js \ test-function-search-03.js \ + binary_search.html \ + binary_search.coffee \ + binary_search.js \ + binary_search.map \ $(NULL) MOCHITEST_BROWSER_FILES_PARTS = MOCHITEST_BROWSER_TESTS MOCHITEST_BROWSER_PAGES diff --git a/browser/devtools/debugger/test/binary_search.coffee b/browser/devtools/debugger/test/binary_search.coffee new file mode 100644 index 00000000000..e3dacdaaab5 --- /dev/null +++ b/browser/devtools/debugger/test/binary_search.coffee @@ -0,0 +1,18 @@ +# Uses a binary search algorithm to locate a value in the specified array. +window.binary_search = (items, value) -> + + start = 0 + stop = items.length - 1 + pivot = Math.floor (start + stop) / 2 + + while items[pivot] isnt value and start < stop + + # Adjust the search area. + stop = pivot - 1 if value < items[pivot] + start = pivot + 1 if value > items[pivot] + + # Recalculate the pivot. + pivot = Math.floor (stop + start) / 2 + + # Make sure we've found the correct value. + if items[pivot] is value then pivot else -1 \ No newline at end of file diff --git a/browser/devtools/debugger/test/binary_search.html b/browser/devtools/debugger/test/binary_search.html new file mode 100644 index 00000000000..f9615da7c40 --- /dev/null +++ b/browser/devtools/debugger/test/binary_search.html @@ -0,0 +1,12 @@ + + + + + Browser Debugger Source Map Test + + + + + + diff --git a/browser/devtools/debugger/test/binary_search.js b/browser/devtools/debugger/test/binary_search.js new file mode 100644 index 00000000000..ce77903e1b0 --- /dev/null +++ b/browser/devtools/debugger/test/binary_search.js @@ -0,0 +1,30 @@ +// Generated by CoffeeScript 1.6.1 +(function() { + + window.binary_search = function(items, value) { + var pivot, start, stop; + start = 0; + stop = items.length - 1; + pivot = Math.floor((start + stop) / 2); + while (items[pivot] !== value && start < stop) { + if (value < items[pivot]) { + stop = pivot - 1; + } + if (value > items[pivot]) { + start = pivot + 1; + } + pivot = Math.floor((stop + start) / 2); + } + if (items[pivot] === value) { + return pivot; + } else { + return -1; + } + }; + +}).call(this); + +// TODO bug 849069: this should just be "binary_search.map", not a full path. +/* +//@ sourceMappingURL=http://example.com/browser/browser/devtools/debugger/test/binary_search.map +*/ diff --git a/browser/devtools/debugger/test/binary_search.map b/browser/devtools/debugger/test/binary_search.map new file mode 100644 index 00000000000..a86e74783f1 --- /dev/null +++ b/browser/devtools/debugger/test/binary_search.map @@ -0,0 +1,9 @@ +{ + "version": 3, + "file": "binary_search.js", + "sources": [ + "http://example.com/browser/browser/devtools/debugger/test/binary_search.coffee" + ], + "names": [], + "mappings": ";AACA;CAAA;CAAA,CAAA,CAAuB,EAAA,CAAjB,GAAkB,IAAxB;CAEE,OAAA,UAAA;CAAA,EAAQ,CAAR,CAAA;CAAA,EACQ,CAAR,CAAa,CAAL;CADR,EAEQ,CAAR,CAAA;CAEA,EAA0C,CAAR,CAAtB,MAAN;CAGJ,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,CAAR,CAAQ,GAAR;QAAA;CACA,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,EAAR,GAAA;QADA;CAAA,EAIQ,CAAI,CAAZ,CAAA;CAXF,IAIA;CAUA,GAAA,CAAS;CAAT,YAA8B;MAA9B;AAA0C,CAAD,YAAA;MAhBpB;CAAvB,EAAuB;CAAvB" +} \ No newline at end of file diff --git a/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js b/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js index 93ae5ff0e49..fc3b2efb54b 100644 --- a/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js +++ b/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js @@ -27,19 +27,22 @@ function test() gDebugger = gPane.panelWin; resumed = true; - gDebugger.addEventListener("Debugger:SourceShown", onScriptShown); + gDebugger.addEventListener("Debugger:SourceShown", onSourceShown); - gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { - framesAdded = true; - executeSoon(startTest); - }); + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", + onFramesAdded); executeSoon(function() { gDebuggee.firstCall(); }); }); - function onScriptShown(aEvent) { + function onFramesAdded(aEvent) { + framesAdded = true; + executeSoon(startTest); + } + + function onSourceShown(aEvent) { scriptShown = aEvent.detail.url.indexOf("-02.js") != -1; executeSoon(startTest); } @@ -48,8 +51,8 @@ function test() { if (scriptShown && framesAdded && resumed && !testStarted) { testStarted = true; - gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown); - Services.tm.currentThread.dispatch({ run: performTest }, 0); + gDebugger.removeEventListener("Debugger:SourceShown", onSourceShown); + executeSoon(performTest); } } diff --git a/browser/devtools/debugger/test/browser_dbg_source_maps-01.js b/browser/devtools/debugger/test/browser_dbg_source_maps-01.js new file mode 100644 index 00000000000..c8ff8aae95f --- /dev/null +++ b/browser/devtools/debugger/test/browser_dbg_source_maps-01.js @@ -0,0 +1,156 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can set breakpoints and step through source mapped coffee + * script. + */ + +const TAB_URL = EXAMPLE_URL + "binary_search.html"; + +var gPane = null; +var gTab = null; +var gDebuggee = null; +var gDebugger = null; + +function test() +{ + let scriptShown = false; + let framesAdded = false; + let resumed = false; + let testStarted = false; + + Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", true); + + debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) { + resumed = true; + gTab = aTab; + gDebuggee = aDebuggee; + gPane = aPane; + gDebugger = gPane.panelWin; + + gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown(aEvent) { + gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown); + ok(aEvent.detail.url.indexOf(".coffee") != -1, + "The debugger should show the source mapped coffee script file."); + ok(gDebugger.editor.getText().search(/isnt/) != -1, + "The debugger's editor should have the coffee script source displayed."); + + testSetBreakpoint(); + }); + }); +} + +function testSetBreakpoint() { + let { activeThread } = gDebugger.DebuggerController; + activeThread.interrupt(function (aResponse) { + activeThread.setBreakpoint({ + url: EXAMPLE_URL + "binary_search.coffee", + line: 5 + }, function (aResponse, bpClient) { + ok(!aResponse.error, + "Should be able to set a breakpoint in a coffee script file."); + testSetBreakpointBlankLine(); + }); + }); +} + +function testSetBreakpointBlankLine() { + let { activeThread } = gDebugger.DebuggerController; + activeThread.setBreakpoint({ + url: EXAMPLE_URL + "binary_search.coffee", + line: 3 + }, function (aResponse, bpClient) { + ok(aResponse.actualLocation, + "Because 3 is empty, we should have an actualLocation"); + is(aResponse.actualLocation.url, EXAMPLE_URL + "binary_search.coffee", + "actualLocation.url should be source mapped to the coffee file"); + is(aResponse.actualLocation.line, 2, + "actualLocation.line should be source mapped back to 2"); + testHitBreakpoint(); + }); +} + +function testHitBreakpoint() { + let { activeThread } = gDebugger.DebuggerController; + activeThread.resume(function (aResponse) { + ok(!aResponse.error, "Shouldn't get an error resuming"); + is(aResponse.type, "resumed", "Type should be 'resumed'"); + + activeThread.addOneTimeListener("paused", function (aEvent, aPacket) { + is(aPacket.type, "paused", + "We should now be paused again"); + is(aPacket.why.type, "breakpoint", + "and the reason we should be paused is because we hit a breakpoint"); + + // Check that we stopped at the right place, by making sure that the + // environment is in the state that we expect. + is(aPacket.frame.environment.bindings.variables.start.value, 0, + "'start' is 0"); + is(aPacket.frame.environment.bindings.variables.stop.value.type, "undefined", + "'stop' hasn't been assigned to yet"); + is(aPacket.frame.environment.bindings.variables.pivot.value.type, "undefined", + "'pivot' hasn't been assigned to yet"); + + waitForCaretPos(4, testStepping); + }); + + // This will cause the breakpoint to be hit, and put us back in the paused + // state. + executeSoon(function() { + gDebuggee.binary_search([0, 2, 3, 5, 7, 10], 5); + }); + }); +} + +function testStepping() { + let { activeThread } = gDebugger.DebuggerController; + activeThread.stepIn(function (aResponse) { + ok(!aResponse.error, "Shouldn't get an error resuming"); + is(aResponse.type, "resumed", "Type should be 'resumed'"); + + // After stepping, we will pause again, so listen for that. + activeThread.addOneTimeListener("paused", function (aEvent, aPacket) { + + // Check that we stopped at the right place, by making sure that the + // environment is in the state that we expect. + is(aPacket.frame.environment.bindings.variables.start.value, 0, + "'start' is 0"); + is(aPacket.frame.environment.bindings.variables.stop.value, 5, + "'stop' hasn't been assigned to yet"); + is(aPacket.frame.environment.bindings.variables.pivot.value.type, "undefined", + "'pivot' hasn't been assigned to yet"); + + waitForCaretPos(5, closeDebuggerAndFinish); + }); + }); +} + +function waitForCaretPos(number, callback) +{ + // Poll every few milliseconds until the source editor line is active. + let count = 0; + let intervalID = window.setInterval(function() { + info("count: " + count + " "); + if (++count > 50) { + ok(false, "Timed out while polling for the line."); + window.clearInterval(intervalID); + return closeDebuggerAndFinish(); + } + if (gDebugger.DebuggerView.editor.getCaretPosition().line != number) { + return; + } + // We got the source editor at the expected line, it's safe to callback. + window.clearInterval(intervalID); + callback(); + }, 100); +} + +registerCleanupFunction(function() { + Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false); + removeTab(gTab); + gPane = null; + gTab = null; + gDebuggee = null; + gDebugger = null; +}); diff --git a/toolkit/devtools/debugger/dbg-client.jsm b/toolkit/devtools/debugger/dbg-client.jsm index bd1db6e50a7..03f47948351 100644 --- a/toolkit/devtools/debugger/dbg-client.jsm +++ b/toolkit/devtools/debugger/dbg-client.jsm @@ -412,7 +412,10 @@ DebuggerClient.prototype = { */ attachTab: function DC_attachTab(aTabActor, aOnResponse) { let self = this; - let packet = { to: aTabActor, type: "attach" }; + let packet = { + to: aTabActor, + type: "attach" + }; this.request(packet, function(aResponse) { let tabClient; if (!aResponse.error) { @@ -462,10 +465,17 @@ DebuggerClient.prototype = { * @param function aOnResponse * Called with the response packet and a ThreadClient * (which will be undefined on error). + * @param object aOptions + * Configuration options. + * - useSourceMaps: whether to use source maps or not. */ - attachThread: function DC_attachThread(aThreadActor, aOnResponse) { + attachThread: function DC_attachThread(aThreadActor, aOnResponse, aOptions={}) { let self = this; - let packet = { to: aThreadActor, type: "attach" }; + let packet = { + to: aThreadActor, + type: "attach", + options: aOptions + }; this.request(packet, function(aResponse) { if (!aResponse.error) { var threadClient = new ThreadClient(self, aThreadActor); @@ -522,7 +532,7 @@ DebuggerClient.prototype = { */ _sendRequests: function DC_sendRequests() { let self = this; - this._pendingRequests = this._pendingRequests.filter(function(request) { + this._pendingRequests = this._pendingRequests.filter(function (request) { if (request.to in self._activeRequests) { return true; } @@ -549,7 +559,7 @@ DebuggerClient.prototype = { ? aPacket : this.compat.onPacket(aPacket); - resolve(packet).then(function (aPacket) { + resolve(packet).then((aPacket) => { if (!this._connected) { // Hello packet. this._connected = true; @@ -559,55 +569,53 @@ DebuggerClient.prototype = { return; } - try { - if (!aPacket.from) { - let msg = "Server did not specify an actor, dropping packet: " + - JSON.stringify(aPacket); - Cu.reportError(msg); - dumpn(msg); - return; - } + if (!aPacket.from) { + let msg = "Server did not specify an actor, dropping packet: " + + JSON.stringify(aPacket); + Cu.reportError(msg); + dumpn(msg); + return; + } - let onResponse; - // Don't count unsolicited notifications or pauses as responses. - if (aPacket.from in this._activeRequests && - !(aPacket.type in UnsolicitedNotifications) && - !(aPacket.type == ThreadStateTypes.paused && - aPacket.why.type in UnsolicitedPauses)) { - onResponse = this._activeRequests[aPacket.from].onResponse; - delete this._activeRequests[aPacket.from]; - } + let onResponse; + // Don't count unsolicited notifications or pauses as responses. + if (aPacket.from in this._activeRequests && + !(aPacket.type in UnsolicitedNotifications) && + !(aPacket.type == ThreadStateTypes.paused && + aPacket.why.type in UnsolicitedPauses)) { + onResponse = this._activeRequests[aPacket.from].onResponse; + delete this._activeRequests[aPacket.from]; + } - // Packets that indicate thread state changes get special treatment. - if (aPacket.type in ThreadStateTypes && - aPacket.from in this._threadClients) { - this._threadClients[aPacket.from]._onThreadState(aPacket); - } - // On navigation the server resumes, so the client must resume as well. - // We achieve that by generating a fake resumption packet that triggers - // the client's thread state change listeners. - if (this.activeThread && - aPacket.type == UnsolicitedNotifications.tabNavigated && - aPacket.from in this._tabClients) { - let resumption = { from: this.activeThread._actor, type: "resumed" }; - this.activeThread._onThreadState(resumption); - } - // Only try to notify listeners on events, not responses to requests - // that lack a packet type. - if (aPacket.type) { - this.notify(aPacket.type, aPacket); - } + // Packets that indicate thread state changes get special treatment. + if (aPacket.type in ThreadStateTypes && + aPacket.from in this._threadClients) { + this._threadClients[aPacket.from]._onThreadState(aPacket); + } + // On navigation the server resumes, so the client must resume as well. + // We achieve that by generating a fake resumption packet that triggers + // the client's thread state change listeners. + if (this.activeThread && + aPacket.type == UnsolicitedNotifications.tabNavigated && + aPacket.from in this._tabClients) { + let resumption = { from: this.activeThread._actor, type: "resumed" }; + this.activeThread._onThreadState(resumption); + } + // Only try to notify listeners on events, not responses to requests + // that lack a packet type. + if (aPacket.type) { + this.notify(aPacket.type, aPacket); + } - if (onResponse) { - onResponse(aPacket); - } - } catch(ex) { - dumpn("Error handling response: " + ex + " - stack:\n" + ex.stack); - Cu.reportError(ex + "\n" + ex.stack); + if (onResponse) { + onResponse(aPacket); } this._sendRequests(); - }.bind(this)); + }, function (ex) { + dumpn("Error handling response: " + ex + " - stack:\n" + ex.stack); + Cu.reportError(ex.message + "\n" + ex.stack); + }); }, /** diff --git a/toolkit/devtools/debugger/server/dbg-script-actors.js b/toolkit/devtools/debugger/server/dbg-script-actors.js index a5875e1e3a8..d7fd8a061a9 100644 --- a/toolkit/devtools/debugger/server/dbg-script-actors.js +++ b/toolkit/devtools/debugger/server/dbg-script-actors.js @@ -30,7 +30,6 @@ function ThreadActor(aHooks, aGlobal) this._frameActors = []; this._environmentActors = []; this._hooks = aHooks; - this._sources = {}; this.global = aGlobal; // A cache of prototype chains for objects that have received a @@ -46,6 +45,11 @@ function ThreadActor(aHooks, aGlobal) this.findGlobals = this.globalManager.findGlobals.bind(this); this.onNewGlobal = this.globalManager.onNewGlobal.bind(this); + this.onNewSource = this.onNewSource.bind(this); + + this._options = { + useSourceMaps: false + }; } /** @@ -73,13 +77,21 @@ ThreadActor.prototype = { return this._threadLifetimePool; }, + get sources() { + if (!this._sources) { + this._sources = new ThreadSources(this, this._options.useSourceMaps, + this._allowSource, this.onNewSource); + } + return this._sources; + }, + clearDebuggees: function TA_clearDebuggees() { if (this.dbg) { this.dbg.removeAllDebuggees(); } this.conn.removeActorPool(this._threadLifetimePool || undefined); this._threadLifetimePool = null; - this._sources = {}; + this._sources = null; }, /** @@ -202,6 +214,8 @@ ThreadActor.prototype = { this._state = "attached"; + update(this._options, aRequest.options || {}); + if (!this.dbg) { this._initDebugger(); } @@ -226,15 +240,17 @@ ThreadActor.prototype = { // We already sent a response to this request, don't send one // now. return null; - } catch(e) { - Cu.reportError(e); + } catch (e) { + reportError(e); return { error: "notAttached", message: e.toString() }; } }, onDetach: function TA_onDetach(aRequest) { this.disconnect(); - return { type: "detached" }; + return { + type: "detached" + }; }, /** @@ -245,15 +261,19 @@ ThreadActor.prototype = { * The newest debuggee frame in the stack. * @param object aReason * An object with a 'type' property containing the reason for the pause. + * @param function onPacket + * Hook to modify the packet before it is sent. Feel free to return a + * promise. */ - _pauseAndRespond: function TA__pauseAndRespond(aFrame, aReason) { + _pauseAndRespond: function TA__pauseAndRespond(aFrame, aReason, + onPacket=function (k) k) { try { let packet = this._paused(aFrame); if (!packet) { return undefined; } packet.why = aReason; - this.conn.send(packet); + resolve(onPacket(packet)).then(this.conn.send.bind(this.conn)); return this._nest(); } catch(e) { let msg = "Got an exception during TA__pauseAndRespond: " + e + @@ -268,6 +288,14 @@ ThreadActor.prototype = { * Handle a protocol request to resume execution of the debuggee. */ onResume: function TA_onResume(aRequest) { + if (this._state !== "paused") { + return { + error: "wrongState", + message: "Can't resume when debuggee isn't paused. Current state is '" + + this._state + "'" + }; + } + // 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. @@ -439,14 +467,23 @@ ThreadActor.prototype = { // Return request.count frames, or all remaining // frames if count is not defined. let frames = []; - for (; frame && (!count || i < (start + count)); i++) { + let promises = []; + for (; frame && (!count || i < (start + count)); i++, frame=frame.older) { let form = this._createFrameActor(frame).form(); form.depth = i; frames.push(form); - frame = frame.older; + + let promise = this.sources.getOriginalLocation(form.where.url, + form.where.line) + .then(function (aOrigLocation) { + form.where = aOrigLocation; + }); + promises.push(promise); } - return { frames: frames }; + return resolveAll(promises).then(function () { + return { frames: frames }; + }); }, onReleaseMany: function TA_onReleaseMany(aRequest) { @@ -479,25 +516,58 @@ ThreadActor.prototype = { message: "Breakpoints can only be set while the debuggee is paused."}; } - let location = aRequest.location; - let line = location.line; - if (this.dbg.findScripts({ url: location.url }).length == 0 || line < 0) { - return { error: "noScript" }; - } + // XXX: `originalColumn` is never used. See bug 827639. + let { url: originalSource, + line: originalLine, + column: originalColumn } = aRequest.location; - // Add the breakpoint to the store for later reuse, in case it belongs to a - // script that hasn't appeared yet. - if (!this._breakpointStore[location.url]) { - this._breakpointStore[location.url] = []; - } - let scriptBreakpoints = this._breakpointStore[location.url]; - scriptBreakpoints[line] = { - url: location.url, - line: line, - column: location.column - }; + let locationPromise = this.sources.getGeneratedLocation(originalSource, + originalLine) + return locationPromise.then((aLocation) => { + let line = aLocation.line; + if (this.dbg.findScripts({ url: aLocation.url }).length == 0 || line < 0) { + return { error: "noScript" }; + } - return this._setBreakpoint(location); + // Add the breakpoint to the store for later reuse, in case it belongs to a + // script that hasn't appeared yet. + if (!this._breakpointStore[aLocation.url]) { + this._breakpointStore[aLocation.url] = []; + } + let scriptBreakpoints = this._breakpointStore[aLocation.url]; + scriptBreakpoints[line] = { + url: aLocation.url, + line: line, + column: aLocation.column + }; + + let response = this._setBreakpoint(aLocation); + // If the original location of our generated location is different from + // the original location we attempted to set the breakpoint on, we will + // need to know so that we can set actualLocation on the response. + let originalLocation = this.sources.getOriginalLocation(aLocation.url, + aLocation.line); + + return resolveAll([response, originalLocation]) + .then(([aResponse, {url, line}]) => { + if (aResponse.actualLocation) { + let actualOrigLocation = this.sources.getOriginalLocation( + aResponse.actualLocation.url, aResponse.actualLocation.line); + return actualOrigLocation.then(function ({ url, line }) { + if (url !== originalSource || line !== originalLine) { + aResponse.actualLocation = { url: url, line: line }; + } + return aResponse; + }); + } + + if (url !== originalSource || line !== originalLine) { + aResponse.actualLocation = { url: url, line: line }; + } + + return aResponse; + }); + }); }, /** @@ -608,17 +678,16 @@ ThreadActor.prototype = { * Get the script and source lists from the debugger. */ _discoverScriptsAndSources: function TA__discoverScriptsAndSources() { - for (let s of this.dbg.findScripts()) { - this._addScript(s); - } + return resolveAll([this._addScript(s) + for (s of this.dbg.findScripts())]); }, onSources: function TA_onSources(aRequest) { - this._discoverScriptsAndSources(); - let urls = Object.getOwnPropertyNames(this._sources); - return { - sources: [this._getSource(url).form() for (url of urls)] - }; + return this._discoverScriptsAndSources().then(() => { + return { + sources: [s.form() for (s of this.sources.iter())] + }; + }); }, /** @@ -655,8 +724,8 @@ ThreadActor.prototype = { // We already sent a response to this request, don't send one // now. return null; - } catch(e) { - Cu.reportError(e); + } catch (e) { + reportError(e); return { error: "notInterrupted", message: e.toString() }; } }, @@ -1029,26 +1098,6 @@ ThreadActor.prototype = { return aString.length >= DebuggerServer.LONG_STRING_LENGTH; }, - /** - * Create a source grip for the given script. - */ - sourceGrip: function TA_sourceGrip(aScript) { - // TODO: Once we have Debugger.Source, this should be replaced with a - // weakmap mapping Debugger.Source instances to SourceActor instances. - if (!this.threadLifetimePool.sourceActors) { - this.threadLifetimePool.sourceActors = {}; - } - - if (this.threadLifetimePool.sourceActors[aScript.url]) { - return this.threadLifetimePool.sourceActors[aScript.url].grip(); - } - - let actor = new SourceActor(aScript.url, this); - this.threadLifetimePool.addActor(actor); - this.threadLifetimePool.sourceActors[aScript.url] = actor; - return actor.form(); - }, - // JS Debugger API hooks. /** @@ -1114,6 +1163,14 @@ ThreadActor.prototype = { this._addScript(aScript); }, + onNewSource: function TA_onNewSource(aSource) { + this.conn.send({ + from: this.actorID, + type: "newSource", + source: aSource.form() + }); + }, + /** * Check if scripts from the provided source URL are allowed to be stored in * the cache. @@ -1122,7 +1179,7 @@ ThreadActor.prototype = { * The url of the script's source that will be stored. * @returns true, if the script can be added, false otherwise. */ - _allowSource: function TA__allowScript(aSourceUrl) { + _allowSource: function TA__allowSource(aSourceUrl) { // Ignore anything we don't have a URL for (eval scripts, for example). if (!aSourceUrl) return false; @@ -1146,28 +1203,30 @@ ThreadActor.prototype = { */ _addScript: function TA__addScript(aScript) { if (!this._allowSource(aScript.url)) { - return false; + return resolve(false); } // TODO bug 637572: we should be dealing with sources directly, not // inferring them through scripts. - this._addSource(aScript.url); + return this.sources.sourcesForScript(aScript).then(() => { - // Set any stored breakpoints. - let existing = this._breakpointStore[aScript.url]; - if (existing) { - let endLine = aScript.startLine + aScript.lineCount - 1; - // Iterate over the lines backwards, so that sliding breakpoints don't - // affect the loop. - for (let line = existing.length - 1; line >= 0; line--) { - let bp = existing[line]; - // Limit search to the line numbers contained in the new script. - if (bp && line >= aScript.startLine && line <= endLine) { - this._setBreakpoint(bp); + // Set any stored breakpoints. + let existing = this._breakpointStore[aScript.url]; + if (existing) { + let endLine = aScript.startLine + aScript.lineCount - 1; + // Iterate over the lines backwards, so that sliding breakpoints don't + // affect the loop. + for (let line = existing.length - 1; line >= 0; line--) { + let bp = existing[line]; + // Limit search to the line numbers contained in the new script. + if (bp && line >= aScript.startLine && line <= endLine) { + this._setBreakpoint(bp); + } } } - } - return true; + + return true; + }); }, /** @@ -1215,48 +1274,6 @@ ThreadActor.prototype = { return retval; }, - /** - * Add a source to the current set of sources. - * - * Right now this takes an url, but in the future it should - * take a Debugger.Source. - * - * @param string the source URL. - * @returns a SourceActor representing the source. - */ - _addSource: function TA__addSource(aURL) { - if (!this._allowSource(aURL)) { - return false; - } - - if (aURL in this._sources) { - return true; - } - - let actor = new SourceActor(aURL, this); - this.threadLifetimePool.addActor(actor); - this._sources[aURL] = actor; - - this.conn.send({ - from: this.actorID, - type: "newSource", - source: actor.form() - }); - - return true; - }, - - /** - * Get the source actor for the given URL. - */ - _getSource: function TA__getSource(aUrl) { - let source = this._sources[aUrl]; - if (!source) { - throw new Error("No source for '" + aUrl + "'"); - } - return source; - }, - }; ThreadActor.prototype.requestTypes = { @@ -1340,7 +1357,7 @@ PauseScopedActor.prototype = { error: "wrongState", message: this.constructor.name + " actors can only be accessed while the thread is paused." - } + }; } }; @@ -1362,7 +1379,8 @@ SourceActor.prototype = { constructor: SourceActor, actorPrefix: "source", - get threadActor() { return this._threadActor; }, + get threadActor() this._threadActor, + get url() this._url, form: function SA_form() { return { @@ -1382,10 +1400,9 @@ SourceActor.prototype = { * Handler for the "source" packet. */ onSource: function SA_onSource(aRequest) { - return this - ._loadSource() + return fetch(this._url) .then(function(aSource) { - return this._threadActor.createValueGrip( + return this.threadActor.createValueGrip( aSource, this.threadActor.threadLifetimePool); }.bind(this)) .then(function (aSourceGrip) { @@ -1394,121 +1411,17 @@ SourceActor.prototype = { source: aSourceGrip }; }.bind(this), function (aError) { + let msg = "Got an exception during SA_onSource: " + aError + + "\n" + aError.stack; + Cu.reportError(msg); + dumpn(msg); return { "from": this.actorID, "error": "loadSourceError", "message": "Could not load the source for " + this._url + "." }; }.bind(this)); - }, - - /** - * Convert a given string, encoded in a given character set, to unicode. - * @param string aString - * A string. - * @param string aCharset - * A character set. - * @return string - * A unicode string. - */ - _convertToUnicode: function SS__convertToUnicode(aString, aCharset) { - // Decoding primitives. - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] - .createInstance(Ci.nsIScriptableUnicodeConverter); - - try { - converter.charset = aCharset || "UTF-8"; - return converter.ConvertToUnicode(aString); - } catch(e) { - return aString; - } - }, - - /** - * Performs a request to load the desired URL and returns a promise. - * - * @param aURL String - * The URL we will request. - * @returns Promise - * - * XXX: It may be better to use nsITraceableChannel to get to the sources - * without relying on caching when we can (not for eval, etc.): - * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/ - */ - _loadSource: function SA__loadSource() { - let deferred = defer(); - let scheme; - let url = this._url.split(" -> ").pop(); - - try { - scheme = Services.io.extractScheme(url); - } catch (e) { - // In the xpcshell tests, the script url is the absolute path of the test - // file, which will make a malformed URI error be thrown. Add the file - // scheme prefix ourselves. - url = "file://" + url; - scheme = Services.io.extractScheme(url); - } - - switch (scheme) { - case "file": - case "chrome": - case "resource": - try { - NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) { - if (!Components.isSuccessCode(aStatus)) { - deferred.reject(new Error("Request failed")); - return; - } - - let source = NetUtil.readInputStreamToString(aStream, aStream.available()); - deferred.resolve(this._convertToUnicode(source)); - aStream.close(); - }.bind(this)); - } catch (ex) { - deferred.reject(new Error("Request failed")); - } - break; - - default: - let channel; - try { - channel = Services.io.newChannel(url, null, null); - } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") { - // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but - // newChannel won't be able to handle it. - url = "file:///" + url; - channel = Services.io.newChannel(url, null, null); - } - let chunks = []; - let streamListener = { - onStartRequest: function(aRequest, aContext, aStatusCode) { - if (!Components.isSuccessCode(aStatusCode)) { - deferred.reject("Request failed"); - } - }, - onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) { - chunks.push(NetUtil.readInputStreamToString(aStream, aCount)); - }, - onStopRequest: function(aRequest, aContext, aStatusCode) { - if (!Components.isSuccessCode(aStatusCode)) { - deferred.reject("Request failed"); - return; - } - - deferred.resolve(this._convertToUnicode(chunks.join(""), - channel.contentCharset)); - }.bind(this) - }; - - channel.loadFlags = channel.LOAD_FROM_CACHE; - channel.asyncOpen(streamListener, null); - break; - } - - return deferred.promise; } - }; SourceActor.prototype.requestTypes = { @@ -2095,7 +2008,14 @@ BreakpointActor.prototype = { hit: function BA_hit(aFrame) { // TODO: add the rest of the breakpoints on that line (bug 676602). let reason = { type: "breakpoint", actors: [ this.actorID ] }; - return this.threadActor._pauseAndRespond(aFrame, reason); + return this.threadActor._pauseAndRespond(aFrame, reason, (aPacket) => { + let { url, line } = aPacket.frame.where; + return this.threadActor.sources.getOriginalLocation(url, line) + .then(function (aOrigPosition) { + aPacket.frame.where = aOrigPosition; + return aPacket; + }); + }); }, /** @@ -2417,6 +2337,183 @@ update(ChromeDebuggerActor.prototype, { }); +/** + * Manages the sources for a thread. Handles source maps, locations in the + * sources, etc for ThreadActors. + */ +function ThreadSources(aThreadActor, aUseSourceMaps, + aAllowPredicate, aOnNewSource) { + this._thread = aThreadActor; + this._useSourceMaps = aUseSourceMaps; + this._allow = aAllowPredicate; + this._onNewSource = aOnNewSource; + + // source map URL --> promise of SourceMapConsumer + this._sourceMaps = Object.create(null); + // generated source url --> promise of SourceMapConsumer + this._sourceMapsByGeneratedSource = Object.create(null); + // original source url --> promise of SourceMapConsumer + this._sourceMapsByOriginalSource = Object.create(null); + // source url --> SourceActor + this._sourceActors = Object.create(null); + // original url --> generated url + this._generatedUrlsByOriginalUrl = Object.create(null); +} + +ThreadSources.prototype = { + /** + * Add a source to the current set of sources. + * + * Right now this takes a URL, but in the future it should + * take a Debugger.Source. See bug 637572. + * + * @param string the source URL. + * @returns a SourceActor representing the source or null. + */ + source: function TS_source(aURL) { + if (!this._allow(aURL)) { + return null; + } + + if (aURL in this._sourceActors) { + return this._sourceActors[aURL]; + } + + let actor = new SourceActor(aURL, this._thread); + this._thread.threadLifetimePool.addActor(actor); + this._sourceActors[aURL] = actor; + try { + this._onNewSource(actor); + } catch (e) { + reportError(e); + } + return actor; + }, + + /** + * Add all of the sources associated with the given script. + */ + sourcesForScript: function TS_sourcesForScript(aScript) { + if (!this._useSourceMaps || !aScript.sourceMapURL) { + return resolve([this.source(aScript.url)].filter(isNotNull)); + } + + return this.sourceMap(aScript) + .then((aSourceMap) => { + return [ + this.source(s) for (s of aSourceMap.sources) + ]; + }, (e) => { + reportError(e); + delete this._sourceMaps[aScript.sourceMapURL]; + delete this._sourceMapsByGeneratedSource[aScript.url]; + return [this.source(aScript.url)]; + }) + .then(function (aSources) { + return aSources.filter(isNotNull); + }); + }, + + /** + * Add the source map for the given script. + */ + sourceMap: function TS_sourceMap(aScript) { + if (aScript.url in this._sourceMapsByGeneratedSource) { + return this._sourceMapsByGeneratedSource[aScript.url]; + } + dbg_assert(aScript.sourceMapURL); + let map = this._fetchSourceMap(aScript.sourceMapURL) + .then((aSourceMap) => { + for (let s of aSourceMap.sources) { + this._generatedUrlsByOriginalUrl[s] = aScript.url; + this._sourceMapsByOriginalSource[s] = resolve(aSourceMap); + } + return aSourceMap; + }); + this._sourceMapsByGeneratedSource[aScript.url] = map; + return map; + }, + + /** + * Fetch the source map located at the given url. + */ + _fetchSourceMap: function TS__featchSourceMap(aSourceMapURL) { + if (aSourceMapURL in this._sourceMaps) { + return this._sourceMaps[aSourceMapURL]; + } else { + let promise = fetch(aSourceMapURL).then(function (rawSourceMap) { + return new SourceMapConsumer(rawSourceMap); + }); + this._sourceMaps[aSourceMapURL] = promise; + return promise; + } + }, + + /** + * Returns a promise for the location in the original source if the source is + * source mapped, otherwise a promise of the same location. + * + * TODO bug 637572: take/return a column + */ + getOriginalLocation: function TS_getOriginalLocation(aSourceUrl, aLine) { + if (aSourceUrl in this._sourceMapsByGeneratedSource) { + return this._sourceMapsByGeneratedSource[aSourceUrl] + .then(function (aSourceMap) { + let { source, line } = aSourceMap.originalPositionFor({ + source: aSourceUrl, + line: aLine, + column: Infinity + }); + return { + url: source, + line: line + }; + }); + } + + // No source map + return resolve({ + url: aSourceUrl, + line: aLine + }); + }, + + /** + * Returns a promise of the location in the generated source corresponding to + * the original source and line given. + * + * TODO bug 637572: take/return a column + */ + getGeneratedLocation: function TS_getGeneratedLocation(aSourceUrl, aLine) { + if (aSourceUrl in this._sourceMapsByOriginalSource) { + return this._sourceMapsByOriginalSource[aSourceUrl] + .then((aSourceMap) => { + let { line } = aSourceMap.generatedPositionFor({ + source: aSourceUrl, + line: aLine, + column: Infinity + }); + return { + url: this._generatedUrlsByOriginalUrl[aSourceUrl], + line: line + }; + }); + } + + // No source map + return resolve({ + url: aSourceUrl, + line: aLine + }); + }, + + iter: function TS_iter() { + for (let url in this._sourceActors) { + yield this._sourceActors[url]; + } + } +}; + // Utility functions. /** @@ -2437,3 +2534,126 @@ function update(aTarget, aNewAttrs) { } } } + +/** + * Returns true if its argument is not null. + */ +function isNotNull(aThing) { + return aThing !== null; +} + +/** + * Performs a request to load the desired URL and returns a promise. + * + * @param aURL String + * The URL we will request. + * @returns Promise + * + * XXX: It may be better to use nsITraceableChannel to get to the sources + * without relying on caching when we can (not for eval, etc.): + * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/ + */ +function fetch(aURL) { + let deferred = defer(); + let scheme; + let url = aURL.split(" -> ").pop(); + let charset; + + try { + scheme = Services.io.extractScheme(url); + } catch (e) { + // In the xpcshell tests, the script url is the absolute path of the test + // file, which will make a malformed URI error be thrown. Add the file + // scheme prefix ourselves. + url = "file://" + url; + scheme = Services.io.extractScheme(url); + } + + switch (scheme) { + case "file": + case "chrome": + case "resource": + try { + NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) { + if (!Components.isSuccessCode(aStatus)) { + deferred.reject("Request failed: " + url); + return; + } + + let source = NetUtil.readInputStreamToString(aStream, aStream.available()); + deferred.resolve(source); + aStream.close(); + }); + } catch (ex) { + deferred.reject("Request failed: " + url); + } + break; + + default: + let channel; + try { + channel = Services.io.newChannel(url, null, null); + } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") { + // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but + // newChannel won't be able to handle it. + url = "file:///" + url; + channel = Services.io.newChannel(url, null, null); + } + let chunks = []; + let streamListener = { + onStartRequest: function(aRequest, aContext, aStatusCode) { + if (!Components.isSuccessCode(aStatusCode)) { + deferred.reject("Request failed: " + url); + } + }, + onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) { + chunks.push(NetUtil.readInputStreamToString(aStream, aCount)); + }, + onStopRequest: function(aRequest, aContext, aStatusCode) { + if (!Components.isSuccessCode(aStatusCode)) { + deferred.reject("Request failed: " + url); + return; + } + + charset = channel.contentCharset; + deferred.resolve(chunks.join("")); + } + }; + + channel.loadFlags = channel.LOAD_FROM_CACHE; + channel.asyncOpen(streamListener, null); + break; + } + + return deferred.promise.then(function (source) { + return convertToUnicode(source, charset); + }); +} + +/** + * Convert a given string, encoded in a given character set, to unicode. + * + * @param string aString + * A string. + * @param string aCharset + * A character set. + */ +function convertToUnicode(aString, aCharset=null) { + // Decoding primitives. + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + try { + converter.charset = aCharset || "UTF-8"; + return converter.ConvertToUnicode(aString); + } catch(e) { + return aString; + } +} + +/** + * Report the given error in the error console and to stdout. + */ +function reportError(aError) { + Cu.reportError(aError); + dumpn(aError.message + ":\n" + aError.stack); +} diff --git a/toolkit/devtools/debugger/server/dbg-server.js b/toolkit/devtools/debugger/server/dbg-server.js index d7a89b8202a..74c3fd1500e 100644 --- a/toolkit/devtools/debugger/server/dbg-server.js +++ b/toolkit/devtools/debugger/server/dbg-server.js @@ -26,6 +26,12 @@ addDebuggerToGlobal(this); Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); const { defer, resolve, reject } = Promise; +let promisedArray = Promise.promised(Array); +function resolveAll(aPromises) { + return promisedArray.apply(null, aPromises); +}; + +Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); function dumpn(str) { if (wantLogging) { @@ -600,6 +606,17 @@ DebuggerServerConnection.prototype = { return null; }, + _unknownError: function DSC__unknownError(aPrefix, aError) { + let errorString = safeErrorString(aError); + errorString += "\n" + aError.stack; + Cu.reportError(errorString); + dumpn(errorString); + return { + error: "unknownError", + message: (aPrefix + "': " + errorString) + }; + }, + // Transport hooks. /** @@ -622,12 +639,9 @@ DebuggerServerConnection.prototype = { try { instance = new actor(); } catch (e) { - Cu.reportError(e); - this.transport.send({ - error: "unknownError", - message: ("error occurred while creating actor '" + actor.name + - "': " + safeErrorString(e)) - }); + this.transport.send(this._unknownError( + "Error occurred while creating actor '" + actor.name, + e)); } instance.parentID = actor.parentID; // We want the newly-constructed actor to completely replace the factory @@ -639,16 +653,14 @@ DebuggerServerConnection.prototype = { } var ret = null; - // Dispatch the request to the actor. if (actor.requestTypes && actor.requestTypes[aPacket.type]) { try { ret = actor.requestTypes[aPacket.type].bind(actor)(aPacket); } catch(e) { - Cu.reportError(e); - ret = { error: "unknownError", - message: ("error occurred while processing '" + aPacket.type + - "' request: " + safeErrorString(e)) }; + this.transport.send(this._unknownError( + "error occurred while processing '" + aPacket.type, + e)); } } else { ret = { error: "unrecognizedPacketType", @@ -663,12 +675,19 @@ DebuggerServerConnection.prototype = { return; } - resolve(ret).then(function(returnPacket) { - if (!returnPacket.from) { - returnPacket.from = aPacket.to; - } - this.transport.send(returnPacket); - }.bind(this)); + resolve(ret) + .then(null, (e) => { + return this._unknownError( + "error occurred while processing '" + aPacket.type, + e); + }) + .then(function (aResponse) { + if (!aResponse.from) { + aResponse.from = aPacket.to; + } + return aResponse; + }) + .then(this.transport.send.bind(this.transport)); }, /** diff --git a/toolkit/devtools/debugger/tests/unit/head_dbg.js b/toolkit/devtools/debugger/tests/unit/head_dbg.js index 1e13cf8b816..525ae7e4655 100644 --- a/toolkit/devtools/debugger/tests/unit/head_dbg.js +++ b/toolkit/devtools/debugger/tests/unit/head_dbg.js @@ -111,7 +111,7 @@ function getTestGlobalContext(aClient, aName, aCallback) { function attachTestGlobalClient(aClient, aName, aCallback) { getTestGlobalContext(aClient, aName, function(aContext) { - aClient.attachThread(aContext.actor, aCallback); + aClient.attachThread(aContext.actor, aCallback, { useSourceMaps: true }); }); } @@ -147,7 +147,7 @@ function attachTestTabAndResume(aClient, aName, aCallback) { aThreadClient.resume(function (aResponse) { aCallback(aResponse, aTabClient, aThreadClient); }); - }); + }, { useSourceMaps: true }); }); } diff --git a/toolkit/devtools/debugger/tests/unit/test_breakpoint-04.js b/toolkit/devtools/debugger/tests/unit/test_breakpoint-04.js index 6c91aa956ef..c83c26c445a 100644 --- a/toolkit/devtools/debugger/tests/unit/test_breakpoint-04.js +++ b/toolkit/devtools/debugger/tests/unit/test_breakpoint-04.js @@ -61,8 +61,8 @@ function test_child_breakpoint() gDebuggee.eval("var line0 = Error().lineNumber;\n" + "function foo() {\n" + // line0 + 1 - " this.a = 1;\n" + // line0 + 2 - " this.b = 2;\n" + // line0 + 3 + " this.a = 1;\n" + // line0 + 2 + " this.b = 2;\n" + // line0 + 3 "}\n" + // line0 + 4 "debugger;\n" + // line0 + 5 "foo();\n"); // line0 + 6 diff --git a/toolkit/devtools/debugger/tests/unit/test_dbgactor.js b/toolkit/devtools/debugger/tests/unit/test_dbgactor.js index 9802638048e..131acda9fb2 100644 --- a/toolkit/devtools/debugger/tests/unit/test_dbgactor.js +++ b/toolkit/devtools/debugger/tests/unit/test_dbgactor.js @@ -38,6 +38,7 @@ function run_test() function test_attach(aContext) { gClient.request({ to: aContext.actor, type: "attach" }, function(aResponse) { + do_check_true(!aResponse.error); do_check_eq(aResponse.type, "paused"); // Resume the thread and test the debugger statement. diff --git a/toolkit/devtools/debugger/tests/unit/test_listsources-02.js b/toolkit/devtools/debugger/tests/unit/test_listsources-02.js index 044f5eaba26..91eabf3df6b 100644 --- a/toolkit/devtools/debugger/tests/unit/test_listsources-02.js +++ b/toolkit/devtools/debugger/tests/unit/test_listsources-02.js @@ -47,6 +47,6 @@ function test_listing_zero_sources() "Should only send one sources request at most, even though we" + " might have had to send one to determine feature support."); - finishClient(gClient); + finishClient(gClient); }); } diff --git a/toolkit/devtools/debugger/tests/unit/test_listsources-03.js b/toolkit/devtools/debugger/tests/unit/test_listsources-03.js new file mode 100644 index 00000000000..7325f4a887e --- /dev/null +++ b/toolkit/devtools/debugger/tests/unit/test_listsources-03.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check getSources functionality when there are lots of sources. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-sources"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect(function () { + attachTestGlobalClientAndResume(gClient, "test-sources", function (aResponse, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_listsources(); + }); + }); + do_test_pending(); +} + +function test_simple_listsources() +{ + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + gThreadClient.getSources(function (aResponse) { + do_check_true( + !aResponse.error, + "There shouldn't be an error fetching large amounts of sources."); + + do_check_true(aResponse.sources.some(function (s) { + return s.url.match(/foo-999.js$/); + })); + + gThreadClient.resume(function () { + finishClient(gClient); + }); + }); + }); + + for (let i = 0; i < 1000; i++) { + Cu.evalInSandbox("function foo###() {return ###;}".replace(/###/g, i), + gDebuggee, + "1.8", + "http://example.com/foo-" + i + ".js", + 1); + } + gDebuggee.eval("debugger;"); +} diff --git a/toolkit/devtools/debugger/tests/unit/test_profiler_actor.js b/toolkit/devtools/debugger/tests/unit/test_profiler_actor.js index ef38552ccbe..64d11bcea9a 100644 --- a/toolkit/devtools/debugger/tests/unit/test_profiler_actor.js +++ b/toolkit/devtools/debugger/tests/unit/test_profiler_actor.js @@ -114,21 +114,15 @@ function test_profile(aClient, aProfiler) do_check_eq(typeof aResponse.profile.threads[0].samples, "object"); do_check_neq(aResponse.profile.threads[0].samples.length, 0); - function some(array, cb) { - for (var i = array.length; i; i--) { - if (cb(array[i - 1])) - return true; - } - return false; - } + let location = stack.name + " (" + stack.filename + ":" + funcLine + ")"; // At least one sample is expected to have been in the busy wait above. - do_check_true(some(aResponse.profile.threads[0].samples, function(sample) { + do_check_true(aResponse.profile.threads[0].samples.some(function(sample) { return sample.name == "(root)" && typeof sample.frames == "object" && sample.frames.length != 0 && sample.frames.some(function(f) { return (f.line == stack.lineNumber) && - (f.location == stack.name + " (" + stack.filename + ":" + funcLine + ")"); + (f.location == location); }); })); @@ -161,9 +155,13 @@ function test_profiler_status() var profiler = aResponse.profilerActor; do_check_false(Profiler.IsActive()); - client.request({ to: profiler, type: "startProfiler", features: [] }, (aResponse) => { + client.request({ + to: profiler, + type: "startProfiler", + features: [] + }, function (aResponse) { do_check_true(Profiler.IsActive()); - client.close(function () {}); + client.close(); }); }); }); diff --git a/toolkit/devtools/debugger/tests/unit/test_sourcemaps-01.js b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-01.js new file mode 100644 index 00000000000..3c00183ccd6 --- /dev/null +++ b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-01.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check basic source map integration with the "newSource" packet in the RDP. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +Components.utils.import('resource:///modules/devtools/SourceMap.jsm'); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect(function() { + attachTestGlobalClientAndResume(gClient, "test-source-map", function(aResponse, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_source_map(); + }); + }); + do_test_pending(); +} + +function test_simple_source_map() +{ + // Because we are source mapping, we should be notified of a.js, b.js, and + // c.js as sources, and shouldn't receive abc.js or test_sourcemaps-01.js. + let expectedSources = new Set(["http://example.com/www/js/a.js", + "http://example.com/www/js/b.js", + "http://example.com/www/js/c.js"]); + + gClient.addListener("newSource", function _onNewSource(aEvent, aPacket) { + do_check_eq(aEvent, "newSource"); + do_check_eq(aPacket.type, "newSource"); + do_check_true(!!aPacket.source); + + do_check_true(expectedSources.has(aPacket.source.url), + "The source url should be one of our original sources."); + expectedSources.delete(aPacket.source.url); + + if (expectedSources.size === 0) { + gClient.removeListener("newSource", _onNewSource); + finishClient(gClient); + } + }); + + let { code, map } = (new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"), + new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"), + new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"), + ])).toStringWithSourceMap({ + file: "abc.js", + sourceRoot: "http://example.com/www/js/" + }); + + code += "//@ sourceMappingURL=data:text/json;base64," + btoa(map.toString()); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/www/js/abc.js", 1); +} diff --git a/toolkit/devtools/debugger/tests/unit/test_sourcemaps-02.js b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-02.js new file mode 100644 index 00000000000..4310884a08b --- /dev/null +++ b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-02.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check basic source map integration with the "sources" packet in the RDP. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +Components.utils.import("resource:///modules/devtools/SourceMap.jsm"); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect(function() { + attachTestGlobalClientAndResume(gClient, "test-source-map", function(aResponse, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_source_map(); + }); + }); + do_test_pending(); +} + +function test_simple_source_map() +{ + // Because we are source mapping, we should be notified of a.js, b.js, and + // c.js as sources, and shouldn"t receive abc.js or test_sourcemaps-01.js. + let expectedSources = new Set(["http://example.com/www/js/a.js", + "http://example.com/www/js/b.js", + "http://example.com/www/js/c.js"]); + + let numNewSources = 0; + + gClient.addListener("newSource", function _onNewSource(aEvent, aPacket) { + if (++numNewSources !== 3) { + return; + } + gClient.removeListener("newSource", _onNewSource); + + gThreadClient.getSources(function (aResponse) { + do_check_true(!aResponse.error, "Should not get an error"); + + for (let s of aResponse.sources) { + do_check_true(expectedSources.has(s.url), + "The source's url should be one of our original sources"); + expectedSources.delete(s.url); + } + + do_check_eq(expectedSources.size, 0, + "Shouldn't be expecting any more sources"); + + finishClient(gClient); + }); + }); + + let { code, map } = (new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"), + new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"), + new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"), + ])).toStringWithSourceMap({ + file: "abc.js", + sourceRoot: "http://example.com/www/js/" + }); + + code += "//@ sourceMappingURL=data:text/json;base64," + btoa(map.toString()); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/www/js/abc.js", 1); +} diff --git a/toolkit/devtools/debugger/tests/unit/test_sourcemaps-03.js b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-03.js new file mode 100644 index 00000000000..802277861c9 --- /dev/null +++ b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-03.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check setting breakpoints in source mapped sources. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +Components.utils.import('resource:///modules/devtools/SourceMap.jsm'); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect(function() { + attachTestGlobalClientAndResume(gClient, "test-source-map", function(aResponse, aThreadClient) { + gThreadClient = aThreadClient; + test_simple_source_map(); + }); + }); + do_test_pending(); +} + +function testBreakpointMapping(aName, aCallback) +{ + // Pause so we can set a breakpoint. + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_true(!aPacket.error); + do_check_eq(aPacket.why.type, "debuggerStatement"); + + gThreadClient.setBreakpoint({ + url: "http://example.com/www/js/" + aName + ".js", + // Setting the breakpoint on an empty line so that it is pushed down one + // line and we can check the source mapped actualLocation later. + line: 3, + column: 0 + }, function (aResponse) { + do_check_true(!aResponse.error); + + // Actual location should come back source mapped still so that + // breakpoints are displayed in the UI correctly, etc. + do_check_eq(aResponse.actualLocation.line, 4); + do_check_eq(aResponse.actualLocation.url, + "http://example.com/www/js/" + aName + ".js"); + + // The eval will cause us to resume, then we get an unsolicited pause + // because of our breakpoint, we resume again to finish the eval, and + // finally receive our last pause which has the result of the client + // evaluation. + gThreadClient.eval(null, aName + "()", function (aResponse) { + do_check_true(!aResponse.error, "Shouldn't be an error resuming to eval"); + do_check_eq(aResponse.type, "resumed"); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "breakpoint"); + // Assert that we paused because of the breakpoint at the correct + // location in the code by testing that the value of `ret` is still + // undefined. + do_check_eq(aPacket.frame.environment.bindings.variables.ret.value.type, + "undefined"); + + gThreadClient.resume(function (aResponse) { + do_check_true(!aResponse.error); + + gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) { + do_check_eq(aPacket.why.type, "clientEvaluated"); + do_check_eq(aPacket.why.frameFinished.return, aName); + + gThreadClient.resume(function (aResponse) { + do_check_true(!aResponse.error); + aCallback(); + }); + }); + }); + }); + }); + }); + }); + + gDebuggee.eval("(" + function () { + debugger; + } + "());"); +} + +function test_simple_source_map() +{ + let expectedSources = new Set([ + "http://example.com/www/js/a.js", + "http://example.com/www/js/b.js", + "http://example.com/www/js/c.js" + ]); + + gClient.addListener("newSource", function _onNewSource(aEvent, aPacket) { + expectedSources.delete(aPacket.source.url); + if (expectedSources.size > 0) { + return; + } + gClient.removeListener("newSource", _onNewSource); + + testBreakpointMapping("a", function () { + testBreakpointMapping("b", function () { + testBreakpointMapping("c", function () { + finishClient(gClient); + }); + }); + }); + }); + + let a = new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "function a() {\n"), + new SourceNode(2, 0, "a.js", " var ret;\n"), + new SourceNode(3, 0, "a.js", " // Empty line\n"), + new SourceNode(4, 0, "a.js", " ret = 'a';\n"), + new SourceNode(5, 0, "a.js", " return ret;\n"), + new SourceNode(6, 0, "a.js", "}\n") + ]); + let b = new SourceNode(null, null, null, [ + new SourceNode(1, 0, "b.js", "function b() {\n"), + new SourceNode(2, 0, "b.js", " var ret;\n"), + new SourceNode(3, 0, "b.js", " // Empty line\n"), + new SourceNode(4, 0, "b.js", " ret = 'b';\n"), + new SourceNode(5, 0, "b.js", " return ret;\n"), + new SourceNode(6, 0, "b.js", "}\n") + ]); + let c = new SourceNode(null, null, null, [ + new SourceNode(1, 0, "c.js", "function c() {\n"), + new SourceNode(2, 0, "c.js", " var ret;\n"), + new SourceNode(3, 0, "c.js", " // Empty line\n"), + new SourceNode(4, 0, "c.js", " ret = 'c';\n"), + new SourceNode(5, 0, "c.js", " return ret;\n"), + new SourceNode(6, 0, "c.js", "}\n") + ]); + + let { code, map } = (new SourceNode(null, null, null, [ + a, b, c + ])).toStringWithSourceMap({ + file: "http://example.com/www/js/abc.js", + sourceRoot: "http://example.com/www/js/" + }); + + code += "//@ sourceMappingURL=data:text/json;base64," + btoa(map.toString()); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + "http://example.com/www/js/abc.js", 1); +} diff --git a/toolkit/devtools/debugger/tests/unit/testcompatactors.js b/toolkit/devtools/debugger/tests/unit/testcompatactors.js index 7eb44846ff2..0b778e51e1b 100644 --- a/toolkit/devtools/debugger/tests/unit/testcompatactors.js +++ b/toolkit/devtools/debugger/tests/unit/testcompatactors.js @@ -30,26 +30,26 @@ function createRootActor() }; actor.thread.requestTypes["scripts"] = function (aRequest) { - this._discoverScriptsAndSources(); - - let scripts = []; - for (let s of this.dbg.findScripts()) { - if (!s.url) { - continue; + return this._discoverScriptsAndSources().then(function () { + let scripts = []; + for (let s of this.dbg.findScripts()) { + if (!s.url) { + continue; + } + let script = { + url: s.url, + startLine: s.startLine, + lineCount: s.lineCount, + source: this.sources.source(s.url).form() + }; + scripts.push(script); } - let script = { - url: s.url, - startLine: s.startLine, - lineCount: s.lineCount, - source: this._getSource(s.url).form() - }; - scripts.push(script); - } - return { - from: this.actorID, - scripts: scripts - }; + return { + from: this.actorID, + scripts: scripts + }; + }.bind(this)); }; // Pretend that we do not know about the "sources" packet to force the @@ -70,7 +70,7 @@ function createRootActor() url: aScript.url, startLine: aScript.startLine, lineCount: aScript.lineCount, - source: actor.thread._getSource(aScript.url).form() + source: actor.thread.sources.source(aScript.url).form() }); }; }(actor.thread.onNewScript)); diff --git a/toolkit/devtools/debugger/tests/unit/xpcshell.ini b/toolkit/devtools/debugger/tests/unit/xpcshell.ini index 21b3aaa6be1..bdf4d89ad35 100644 --- a/toolkit/devtools/debugger/tests/unit/xpcshell.ini +++ b/toolkit/devtools/debugger/tests/unit/xpcshell.ini @@ -74,9 +74,13 @@ skip-if = toolkit == "gonk" reason = bug 820380 [test_listsources-01.js] [test_listsources-02.js] +[test_listsources-03.js] [test_new_source-01.js] [test_sources_backwards_compat-01.js] [test_sources_backwards_compat-02.js] +[test_sourcemaps-01.js] +[test_sourcemaps-02.js] +[test_sourcemaps-03.js] [test_objectgrips-01.js] [test_objectgrips-02.js] [test_objectgrips-03.js] diff --git a/toolkit/devtools/sourcemap/SourceMap.jsm b/toolkit/devtools/sourcemap/SourceMap.jsm index 0dc4063c10c..b8f9bd4258a 100644 --- a/toolkit/devtools/sourcemap/SourceMap.jsm +++ b/toolkit/devtools/sourcemap/SourceMap.jsm @@ -44,6 +44,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou * - sources: An array of URLs to the original source files. * - names: An array of identifiers which can be referrenced by individual mappings. * - sourceRoot: Optional. The URL root from which all sources are relative. + * - sourcesContent: Optional. An array of contents of the original source files. * - mappings: A string of base64 VLQs which contain the actual mappings. * - file: The generated file this source map is associated with. * @@ -70,6 +71,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou var sources = util.getArg(sourceMap, 'sources'); var names = util.getArg(sourceMap, 'names'); var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null); + var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null); var mappings = util.getArg(sourceMap, 'mappings'); var file = util.getArg(sourceMap, 'file'); @@ -79,7 +81,8 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou this._names = ArraySet.fromArray(names); this._sources = ArraySet.fromArray(sources); - this._sourceRoot = sourceRoot; + this.sourceRoot = sourceRoot; + this.sourcesContent = sourcesContent; this.file = file; // `this._generatedMappings` and `this._originalMappings` hold the parsed @@ -121,7 +124,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou Object.defineProperty(SourceMapConsumer.prototype, 'sources', { get: function () { return this._sources.toArray().map(function (s) { - return this._sourceRoot ? util.join(this._sourceRoot, s) : s; + return this.sourceRoot ? util.join(this.sourceRoot, s) : s; }, this); } }); @@ -165,12 +168,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou if (str.length > 0 && !mappingSeparator.test(str.charAt(0))) { // Original source. temp = base64VLQ.decode(str); - if (aSourceRoot) { - mapping.source = util.join(aSourceRoot, this._sources.at(previousSource + temp.value)); - } - else { - mapping.source = this._sources.at(previousSource + temp.value); - } + mapping.source = this._sources.at(previousSource + temp.value); previousSource += temp.value; str = temp.rest; if (str.length === 0 || mappingSeparator.test(str.charAt(0))) { @@ -204,7 +202,9 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou } this._generatedMappings.push(mapping); - this._originalMappings.push(mapping); + if (typeof mapping.originalLine === 'number') { + this._originalMappings.push(mapping); + } } } @@ -291,11 +291,15 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou this._generatedMappings, "generatedLine", "generatedColumn", - this._compareGeneratedPositions) + this._compareGeneratedPositions); if (mapping) { + var source = util.getArg(mapping, 'source', null); + if (source && this.sourceRoot) { + source = util.join(this.sourceRoot, source); + } return { - source: util.getArg(mapping, 'source', null), + source: source, line: util.getArg(mapping, 'originalLine', null), column: util.getArg(mapping, 'originalColumn', null), name: util.getArg(mapping, 'name', null) @@ -310,6 +314,32 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou }; }; + /** + * Returns the original source content. The only argument is + * the url of the original source file. Returns null if no + * original source content is availible. + */ + SourceMapConsumer.prototype.sourceContentFor = + function SourceMapConsumer_sourceContentFor(aSource) { + if (!this.sourcesContent) { + return null; + } + + if (this.sourceRoot) { + // Try to remove the sourceRoot + var relativeUrl = util.relative(this.sourceRoot, aSource); + if (this._sources.has(relativeUrl)) { + return this.sourcesContent[this._sources.indexOf(relativeUrl)]; + } + } + + if (this._sources.has(aSource)) { + return this.sourcesContent[this._sources.indexOf(aSource)]; + } + + throw new Error('"' + aSource + '" is not in the SourceMap.'); + }; + /** * Returns the generated line and column information for the original source, * line, and column positions provided. The only argument is an object with @@ -332,11 +362,15 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou originalColumn: util.getArg(aArgs, 'column') }; + if (this.sourceRoot) { + needle.source = util.relative(this.sourceRoot, needle.source); + } + var mapping = this._findMapping(needle, this._originalMappings, "originalLine", "originalColumn", - this._compareOriginalPositions) + this._compareOriginalPositions); if (mapping) { return { @@ -359,8 +393,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou * generated line/column in this source map. * * @param Function aCallback - * The function that is called with each mapping. This function should - * not mutate the mapping. + * The function that is called with each mapping. * @param Object aContext * Optional. If specified, this object will be the value of `this` every * time that `aCallback` is called. @@ -388,7 +421,21 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou throw new Error("Unknown order of iteration."); } - mappings.forEach(aCallback, context); + var sourceRoot = this.sourceRoot; + mappings.map(function (mapping) { + var source = mapping.source; + if (source && sourceRoot) { + source = util.join(sourceRoot, source); + } + return { + source: source, + generatedLine: mapping.generatedLine, + generatedColumn: mapping.generatedColumn, + originalLine: mapping.originalLine, + originalColumn: mapping.originalColumn, + name: mapping.name + }; + }).forEach(aCallback, context); }; exports.SourceMapConsumer = SourceMapConsumer; @@ -426,10 +473,37 @@ define('source-map/util', ['require', 'exports', 'module' , ], function(require, function join(aRoot, aPath) { return aPath.charAt(0) === '/' ? aPath - : aRoot.replace(/\/*$/, '') + '/' + aPath; + : aRoot.replace(/\/$/, '') + '/' + aPath; } exports.join = join; + /** + * Because behavior goes wacky when you set `__proto__` on objects, we + * have to prefix all the strings in our set with an arbitrary character. + * + * See https://github.com/mozilla/source-map/pull/31 and + * https://github.com/mozilla/source-map/issues/30 + * + * @param String aStr + */ + function toSetString(aStr) { + return '$' + aStr; + } + exports.toSetString = toSetString; + + function fromSetString(aStr) { + return aStr.substr(1); + } + exports.fromSetString = fromSetString; + + function relative(aRoot, aPath) { + aRoot = aRoot.replace(/\/$/, ''); + return aPath.indexOf(aRoot + '/') === 0 + ? aPath.substr(aRoot.length + 1) + : aPath; + } + exports.relative = relative; + }); /* -*- Mode: js; js-indent-level: 2; -*- */ /* @@ -515,7 +589,9 @@ define('source-map/binary-search', ['require', 'exports', 'module' , ], function * Licensed under the New BSD license. See LICENSE or: * http://opensource.org/licenses/BSD-3-Clause */ -define('source-map/array-set', ['require', 'exports', 'module' , ], function(require, exports, module) { +define('source-map/array-set', ['require', 'exports', 'module' , 'source-map/util'], function(require, exports, module) { + + var util = require('source-map/util'); /** * A data structure which is a combination of an array and a set. Adding a new @@ -539,19 +615,6 @@ define('source-map/array-set', ['require', 'exports', 'module' , ], function(req return set; }; - /** - * Because behavior goes wacky when you set `__proto__` on `this._set`, we - * have to prefix all the strings in our set with an arbitrary character. - * - * See https://github.com/mozilla/source-map/pull/31 and - * https://github.com/mozilla/source-map/issues/30 - * - * @param String aStr - */ - ArraySet.prototype._toSetString = function ArraySet__toSetString (aStr) { - return "$" + aStr; - }; - /** * Add the given string to this set. * @@ -564,7 +627,7 @@ define('source-map/array-set', ['require', 'exports', 'module' , ], function(req } var idx = this._array.length; this._array.push(aStr); - this._set[this._toSetString(aStr)] = idx; + this._set[util.toSetString(aStr)] = idx; }; /** @@ -574,7 +637,7 @@ define('source-map/array-set', ['require', 'exports', 'module' , ], function(req */ ArraySet.prototype.has = function ArraySet_has(aStr) { return Object.prototype.hasOwnProperty.call(this._set, - this._toSetString(aStr)); + util.toSetString(aStr)); }; /** @@ -584,7 +647,7 @@ define('source-map/array-set', ['require', 'exports', 'module' , ], function(req */ ArraySet.prototype.indexOf = function ArraySet_indexOf(aStr) { if (this.has(aStr)) { - return this._set[this._toSetString(aStr)]; + return this._set[util.toSetString(aStr)]; } throw new Error('"' + aStr + '" is not in the set.'); }; @@ -819,10 +882,58 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so this._sources = new ArraySet(); this._names = new ArraySet(); this._mappings = []; + this._sourcesContents = null; } SourceMapGenerator.prototype._version = 3; + /** + * Creates a new SourceMapGenerator based on a SourceMapConsumer + * + * @param aSourceMapConsumer The SourceMap. + */ + SourceMapGenerator.fromSourceMap = + function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) { + var sourceRoot = aSourceMapConsumer.sourceRoot; + var generator = new SourceMapGenerator({ + file: aSourceMapConsumer.file, + sourceRoot: sourceRoot + }); + aSourceMapConsumer.eachMapping(function (mapping) { + var newMapping = { + generated: { + line: mapping.generatedLine, + column: mapping.generatedColumn + } + }; + + if (mapping.source) { + newMapping.source = mapping.source; + if (sourceRoot) { + newMapping.source = util.relative(sourceRoot, newMapping.source); + } + + newMapping.original = { + line: mapping.originalLine, + column: mapping.originalColumn + }; + + if (mapping.name) { + newMapping.name = mapping.name; + } + } + + generator.addMapping(newMapping); + }); + aSourceMapConsumer.sources.forEach(function (sourceFile) { + var content = aSourceMapConsumer.sourceContentFor(sourceFile); + if (content) { + generator.setSourceContent(sourceFile, content); + } + }); + return generator; + }; + /** * Add a single mapping from original source line and column to the generated * source's line and column for this source map being created. The mapping @@ -858,6 +969,110 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so }); }; + /** + * Set the source content for a source file. + */ + SourceMapGenerator.prototype.setSourceContent = + function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) { + var source = aSourceFile; + if (this._sourceRoot) { + source = util.relative(this._sourceRoot, source); + } + + if (aSourceContent !== null) { + // Add the source content to the _sourcesContents map. + // Create a new _sourcesContents map if the property is null. + if (!this._sourcesContents) { + this._sourcesContents = {}; + } + this._sourcesContents[util.toSetString(source)] = aSourceContent; + } else { + // Remove the source file from the _sourcesContents map. + // If the _sourcesContents map is empty, set the property to null. + delete this._sourcesContents[util.toSetString(source)]; + if (Object.keys(this._sourcesContents).length === 0) { + this._sourcesContents = null; + } + } + }; + + /** + * Applies the mappings of a sub-source-map for a specific source file to the + * source map being generated. Each mapping to the supplied source file is + * rewritten using the supplied source map. Note: The resolution for the + * resulting mappings is the minimium of this map and the supplied map. + * + * @param aSourceMapConsumer The source map to be applied. + * @param aSourceFile Optional. The filename of the source file. + * If omitted, SourceMapConsumer's file property will be used. + */ + SourceMapGenerator.prototype.applySourceMap = + function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile) { + // If aSourceFile is omitted, we will use the file property of the SourceMap + if (!aSourceFile) { + aSourceFile = aSourceMapConsumer.file; + } + var sourceRoot = this._sourceRoot; + // Make "aSourceFile" relative if an absolute Url is passed. + if (sourceRoot) { + aSourceFile = util.relative(sourceRoot, aSourceFile); + } + // Applying the SourceMap can add and remove items from the sources and + // the names array. + var newSources = new ArraySet(); + var newNames = new ArraySet(); + + // Find mappings for the "aSourceFile" + this._mappings.forEach(function (mapping) { + if (mapping.source === aSourceFile && mapping.original) { + // Check if it can be mapped by the source map, then update the mapping. + var original = aSourceMapConsumer.originalPositionFor({ + line: mapping.original.line, + column: mapping.original.column + }); + if (original.source !== null) { + // Copy mapping + if (sourceRoot) { + mapping.source = util.relative(sourceRoot, original.source); + } else { + mapping.source = original.source; + } + mapping.original.line = original.line; + mapping.original.column = original.column; + if (original.name !== null && mapping.name !== null) { + // Only use the identifier name if it's an identifier + // in both SourceMaps + mapping.name = original.name; + } + } + } + + var source = mapping.source; + if (source && !newSources.has(source)) { + newSources.add(source); + } + + var name = mapping.name; + if (name && !newNames.has(name)) { + newNames.add(name); + } + + }, this); + this._sources = newSources; + this._names = newNames; + + // Copy sourcesContents of applied map. + aSourceMapConsumer.sources.forEach(function (sourceFile) { + var content = aSourceMapConsumer.sourceContentFor(sourceFile); + if (content) { + if (sourceRoot) { + sourceFile = util.relative(sourceRoot, sourceFile); + } + this.setSourceContent(sourceFile, content); + } + }, this); + }; + /** * A mapping can have one of the three levels of data: * @@ -978,6 +1193,17 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so if (this._sourceRoot) { map.sourceRoot = this._sourceRoot; } + if (this._sourcesContents) { + map.sourcesContent = map.sources.map(function (source) { + if (map.sourceRoot) { + source = util.relative(map.sourceRoot, source); + } + return Object.prototype.hasOwnProperty.call( + this._sourcesContents, util.toSetString(source)) + ? this._sourcesContents[util.toSetString(source)] + : null; + }, this); + } return map; }; @@ -998,9 +1224,10 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so * Licensed under the New BSD license. See LICENSE or: * http://opensource.org/licenses/BSD-3-Clause */ -define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/source-map-generator'], function(require, exports, module) { +define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/source-map-generator', 'source-map/util'], function(require, exports, module) { var SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator; + var util = require('source-map/util'); /** * SourceNodes provide a way to abstract over interpolating/concatenating @@ -1012,15 +1239,121 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ * @param aSource The original source's filename. * @param aChunks Optional. An array of strings which are snippets of * generated JS, or other SourceNodes. + * @param aName The original identifier. */ - function SourceNode(aLine, aColumn, aSource, aChunks) { + function SourceNode(aLine, aColumn, aSource, aChunks, aName) { this.children = []; - this.line = aLine; - this.column = aColumn; - this.source = aSource; + this.sourceContents = {}; + this.line = aLine === undefined ? null : aLine; + this.column = aColumn === undefined ? null : aColumn; + this.source = aSource === undefined ? null : aSource; + this.name = aName === undefined ? null : aName; if (aChunks != null) this.add(aChunks); } + /** + * Creates a SourceNode from generated code and a SourceMapConsumer. + * + * @param aGeneratedCode The generated code + * @param aSourceMapConsumer The SourceMap for the generated code + */ + SourceNode.fromStringWithSourceMap = + function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer) { + // The SourceNode we want to fill with the generated code + // and the SourceMap + var node = new SourceNode(); + + // The generated code + // Processed fragments are removed from this array. + var remainingLines = aGeneratedCode.split('\n'); + + // We need to remember the position of "remainingLines" + var lastGeneratedLine = 1, lastGeneratedColumn = 0; + + // The generate SourceNodes we need a code range. + // To extract it current and last mapping is used. + // Here we store the last mapping. + var lastMapping = null; + + aSourceMapConsumer.eachMapping(function (mapping) { + if (lastMapping === null) { + // We add the generated code until the first mapping + // to the SourceNode without any mapping. + // Each line is added as separate string. + while (lastGeneratedLine < mapping.generatedLine) { + node.add(remainingLines.shift() + "\n"); + lastGeneratedLine++; + } + if (lastGeneratedColumn < mapping.generatedColumn) { + var nextLine = remainingLines[0]; + node.add(nextLine.substr(0, mapping.generatedColumn)); + remainingLines[0] = nextLine.substr(mapping.generatedColumn); + lastGeneratedColumn = mapping.generatedColumn; + } + } else { + // We add the code from "lastMapping" to "mapping": + // First check if there is a new line in between. + if (lastGeneratedLine < mapping.generatedLine) { + var code = ""; + // Associate full lines with "lastMapping" + do { + code += remainingLines.shift() + "\n"; + lastGeneratedLine++; + lastGeneratedColumn = 0; + } while (lastGeneratedLine < mapping.generatedLine); + // When we reached the correct line, we add code until we + // reach the correct column too. + if (lastGeneratedColumn < mapping.generatedColumn) { + var nextLine = remainingLines[0]; + code += nextLine.substr(0, mapping.generatedColumn); + remainingLines[0] = nextLine.substr(mapping.generatedColumn); + lastGeneratedColumn = mapping.generatedColumn; + } + // Create the SourceNode. + addMappingWithCode(lastMapping, code); + } else { + // There is no new line in between. + // Associate the code between "lastGeneratedColumn" and + // "mapping.generatedColumn" with "lastMapping" + var nextLine = remainingLines[0]; + var code = nextLine.substr(0, mapping.generatedColumn - + lastGeneratedColumn); + remainingLines[0] = nextLine.substr(mapping.generatedColumn - + lastGeneratedColumn); + lastGeneratedColumn = mapping.generatedColumn; + addMappingWithCode(lastMapping, code); + } + } + lastMapping = mapping; + }, this); + // We have processed all mappings. + // Associate the remaining code in the current line with "lastMapping" + // and add the remaining lines without any mapping + addMappingWithCode(lastMapping, remainingLines.join("\n")); + + // Copy sourcesContent into SourceNode + aSourceMapConsumer.sources.forEach(function (sourceFile) { + var content = aSourceMapConsumer.sourceContentFor(sourceFile); + if (content) { + node.setSourceContent(sourceFile, content); + } + }); + + return node; + + function addMappingWithCode(mapping, code) { + if (mapping.source === undefined) { + node.add(code); + } else { + node.add(new SourceNode(mapping.originalLine, + mapping.originalColumn, + mapping.source, + code, + mapping.name)); + } + } + }; + /** * Add a chunk of generated JS to this source node. * @@ -1083,7 +1416,10 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ } else { if (chunk !== '') { - aFn(chunk, { source: this.source, line: this.line, column: this.column }); + aFn(chunk, { source: this.source, + line: this.line, + column: this.column, + name: this.name }); } } }, this); @@ -1098,7 +1434,7 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ SourceNode.prototype.join = function SourceNode_join(aSep) { var newChildren; var i; - var len = this.children.length + var len = this.children.length; if (len > 0) { newChildren = []; for (i = 0; i < len-1; i++) { @@ -1132,6 +1468,36 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ return this; }; + /** + * Set the source content for a source file. This will be added to the SourceMapGenerator + * in the sourcesContent field. + * + * @param aSourceFile The filename of the source file + * @param aSourceContent The content of the source file + */ + SourceNode.prototype.setSourceContent = + function SourceNode_setSourceContent(aSourceFile, aSourceContent) { + this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent; + }; + + /** + * Walk over the tree of SourceNodes. The walking function is called for each + * source file content and is passed the filename and source content. + * + * @param aFn The traversal function. + */ + SourceNode.prototype.walkSourceContents = + function SourceNode_walkSourceContents(aFn) { + this.children.forEach(function (chunk) { + if (chunk instanceof SourceNode) { + chunk.walkSourceContents(aFn); + } + }, this); + Object.keys(this.sourceContents).forEach(function (sourceFileKey) { + aFn(util.fromSetString(sourceFileKey), this.sourceContents[sourceFileKey]); + }, this); + }; + /** * Return the string representation of this source node. Walks over the tree * and concatenates all the various snippets together to one string. @@ -1155,25 +1521,36 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ column: 0 }; var map = new SourceMapGenerator(aArgs); + var sourceMappingActive = false; this.walk(function (chunk, original) { generated.code += chunk; - if (original.source != null - && original.line != null - && original.column != null) { + if (original.source !== null + && original.line !== null + && original.column !== null) { map.addMapping({ source: original.source, original: { line: original.line, column: original.column }, + generated: { + line: generated.line, + column: generated.column + }, + name: original.name + }); + sourceMappingActive = true; + } else if (sourceMappingActive) { + map.addMapping({ generated: { line: generated.line, column: generated.column } }); + sourceMappingActive = false; } - chunk.split('').forEach(function (char) { - if (char === '\n') { + chunk.split('').forEach(function (ch) { + if (ch === '\n') { generated.line++; generated.column = 0; } else { @@ -1181,6 +1558,9 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ } }); }); + this.walkSourceContents(function (sourceFile, sourceContent) { + map.setSourceContent(sourceFile, sourceContent); + }); return { code: generated.code, map: map }; }; @@ -1191,6 +1571,6 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ /* -*- Mode: js; js-indent-level: 2; -*- */ /////////////////////////////////////////////////////////////////////////////// -let SourceMapConsumer = require('source-map/source-map-consumer').SourceMapConsumer; -let SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator; -let SourceNode = require('source-map/source-node').SourceNode; +this.SourceMapConsumer = require('source-map/source-map-consumer').SourceMapConsumer; +this.SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator; +this.SourceNode = require('source-map/source-node').SourceNode; diff --git a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm index 5f7c81d529f..ceb12e0141d 100644 --- a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm +++ b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm @@ -15,7 +15,7 @@ Components.utils.import('resource://gre/modules/devtools/Require.jsm'); Components.utils.import('resource://gre/modules/devtools/SourceMap.jsm'); -let EXPORTED_SYMBOLS = [ "define", "runSourceMapTests" ]; +this.EXPORTED_SYMBOLS = [ "define", "runSourceMapTests" ]; /* -*- Mode: js; js-indent-level: 2; -*- */ /* * Copyright 2011 Mozilla Foundation and contributors @@ -78,7 +78,9 @@ define('test/source-map/assert', ['exports'], function (exports) { * Licensed under the New BSD license. See LICENSE or: * http://opensource.org/licenses/BSD-3-Clause */ -define('test/source-map/util', ['require', 'exports', 'module' , ], function(require, exports, module) { +define('test/source-map/util', ['require', 'exports', 'module' , 'lib/source-map/util'], function(require, exports, module) { + + var util = require('source-map/util'); // This is a test mapping which maps functions from two different files // (one.js and two.js) to a minified generated source. @@ -99,6 +101,8 @@ define('test/source-map/util', ['require', 'exports', 'module' , ], function(req // // ONE.foo=function(a){return baz(a);}; // TWO.inc=function(a){return a+1;}; + exports.testGeneratedCode = " ONE.foo=function(a){return baz(a);};\n"+ + " TWO.inc=function(a){return a+1;};"; exports.testMap = { version: 3, file: 'min.js', @@ -107,6 +111,22 @@ define('test/source-map/util', ['require', 'exports', 'module' , ], function(req sourceRoot: '/the/root', mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA' }; + exports.testMapWithSourcesContent = { + version: 3, + file: 'min.js', + names: ['bar', 'baz', 'n'], + sources: ['one.js', 'two.js'], + sourcesContent: [ + ' ONE.foo = function (bar) {\n' + + ' return baz(bar);\n' + + ' };', + ' TWO.inc = function (n) {\n' + + ' return n + 1;\n' + + ' };' + ], + sourceRoot: '/the/root', + mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA' + }; function assertMapping(generatedLine, generatedColumn, originalSource, originalLine, originalColumn, name, map, assert, @@ -125,7 +145,8 @@ define('test/source-map/util', ['require', 'exports', 'module' , ], function(req assert.equal(origMapping.column, originalColumn, 'Incorrect column, expected ' + JSON.stringify(originalColumn) + ', got ' + JSON.stringify(origMapping.column)); - assert.equal(origMapping.source, originalSource, + assert.equal(origMapping.source, + originalSource ? util.join(map._sourceRoot, originalSource) : null, 'Incorrect source, expected ' + JSON.stringify(originalSource) + ', got ' + JSON.stringify(origMapping.source)); } @@ -146,6 +167,111 @@ define('test/source-map/util', ['require', 'exports', 'module' , ], function(req } exports.assertMapping = assertMapping; + function assertEqualMaps(assert, actualMap, expectedMap) { + assert.equal(actualMap.version, expectedMap.version, "version mismatch"); + assert.equal(actualMap.file, expectedMap.file, "file mismatch"); + assert.equal(actualMap.names.length, + expectedMap.names.length, + "names length mismatch: " + + actualMap.names.join(", ") + " != " + expectedMap.names.join(", ")); + for (var i = 0; i < actualMap.names.length; i++) { + assert.equal(actualMap.names[i], + expectedMap.names[i], + "names[" + i + "] mismatch: " + + actualMap.names.join(", ") + " != " + expectedMap.names.join(", ")); + } + assert.equal(actualMap.sources.length, + expectedMap.sources.length, + "sources length mismatch: " + + actualMap.sources.join(", ") + " != " + expectedMap.sources.join(", ")); + for (var i = 0; i < actualMap.sources.length; i++) { + assert.equal(actualMap.sources[i], + expectedMap.sources[i], + "sources[" + i + "] length mismatch: " + + actualMap.sources.join(", ") + " != " + expectedMap.sources.join(", ")); + } + assert.equal(actualMap.sourceRoot, + expectedMap.sourceRoot, + "sourceRoot mismatch: " + + actualMap.sourceRoot + " != " + expectedMap.sourceRoot); + assert.equal(actualMap.mappings, expectedMap.mappings, "mappings mismatch"); + if (actualMap.sourcesContent) { + assert.equal(actualMap.sourcesContent.length, + expectedMap.sourcesContent.length, + "sourcesContent length mismatch"); + for (var i = 0; i < actualMap.sourcesContent.length; i++) { + assert.equal(actualMap.sourcesContent[i], + expectedMap.sourcesContent[i], + "sourcesContent[" + i + "] mismatch"); + } + } + } + exports.assertEqualMaps = assertEqualMaps; + +}); +/* -*- Mode: js; js-indent-level: 2; -*- */ +/* + * Copyright 2011 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE or: + * http://opensource.org/licenses/BSD-3-Clause + */ +define('lib/source-map/util', ['require', 'exports', 'module' , ], function(require, exports, module) { + + /** + * This is a helper function for getting values from parameter/options + * objects. + * + * @param args The object we are extracting values from + * @param name The name of the property we are getting. + * @param defaultValue An optional value to return if the property is missing + * from the object. If this is not specified and the property is missing, an + * error will be thrown. + */ + function getArg(aArgs, aName, aDefaultValue) { + if (aName in aArgs) { + return aArgs[aName]; + } else if (arguments.length === 3) { + return aDefaultValue; + } else { + throw new Error('"' + aName + '" is a required argument.'); + } + } + exports.getArg = getArg; + + function join(aRoot, aPath) { + return aPath.charAt(0) === '/' + ? aPath + : aRoot.replace(/\/$/, '') + '/' + aPath; + } + exports.join = join; + + /** + * Because behavior goes wacky when you set `__proto__` on objects, we + * have to prefix all the strings in our set with an arbitrary character. + * + * See https://github.com/mozilla/source-map/pull/31 and + * https://github.com/mozilla/source-map/issues/30 + * + * @param String aStr + */ + function toSetString(aStr) { + return '$' + aStr; + } + exports.toSetString = toSetString; + + function fromSetString(aStr) { + return aStr.substr(1); + } + exports.fromSetString = fromSetString; + + function relative(aRoot, aPath) { + aRoot = aRoot.replace(/\/$/, ''); + return aPath.indexOf(aRoot + '/') === 0 + ? aPath.substr(aRoot.length + 1) + : aPath; + } + exports.relative = relative; + }); /* -*- Mode: js; js-indent-level: 2; -*- */ /* @@ -167,3 +293,4 @@ function runSourceMapTests(modName, do_throw) { } } +this.runSourceMapTests = runSourceMapTests; diff --git a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js index e3438fc87f6..3b7a59e7e11 100644 --- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js +++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js @@ -15,6 +15,7 @@ Components.utils.import('resource://test/Utils.jsm'); define("test/source-map/test-source-map-consumer", ["require", "exports", "module"], function (require, exports, module) { var SourceMapConsumer = require('source-map/source-map-consumer').SourceMapConsumer; + var SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator; exports['test that we can instantiate with a string or an objects'] = function (assert, util) { assert.doesNotThrow(function () { @@ -97,6 +98,10 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul map.eachMapping(function (mapping) { assert.ok(mapping.generatedLine >= previousLine); + if (mapping.source) { + assert.equal(mapping.source.indexOf(util.testMap.sourceRoot), 0); + } + if (mapping.generatedLine === previousLine) { assert.ok(mapping.generatedColumn >= previousColumn); previousColumn = mapping.generatedColumn; @@ -144,6 +149,112 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul }, context); }; + exports['test that the `sourcesContent` field has the original sources'] = function (assert, util) { + var map = new SourceMapConsumer(util.testMapWithSourcesContent); + var sourcesContent = map.sourcesContent; + + assert.equal(sourcesContent[0], ' ONE.foo = function (bar) {\n return baz(bar);\n };'); + assert.equal(sourcesContent[1], ' TWO.inc = function (n) {\n return n + 1;\n };'); + assert.equal(sourcesContent.length, 2); + }; + + exports['test that we can get the original sources for the sources'] = function (assert, util) { + var map = new SourceMapConsumer(util.testMapWithSourcesContent); + var sources = map.sources; + + assert.equal(map.sourceContentFor(sources[0]), ' ONE.foo = function (bar) {\n return baz(bar);\n };'); + assert.equal(map.sourceContentFor(sources[1]), ' TWO.inc = function (n) {\n return n + 1;\n };'); + assert.equal(map.sourceContentFor("one.js"), ' ONE.foo = function (bar) {\n return baz(bar);\n };'); + assert.equal(map.sourceContentFor("two.js"), ' TWO.inc = function (n) {\n return n + 1;\n };'); + assert.throws(function () { + map.sourceContentFor(""); + }, Error); + assert.throws(function () { + map.sourceContentFor("/the/root/three.js"); + }, Error); + assert.throws(function () { + map.sourceContentFor("three.js"); + }, Error); + }; + + exports['test sourceRoot + generatedPositionFor'] = function (assert, util) { + var map = new SourceMapGenerator({ + sourceRoot: 'foo/bar', + file: 'baz.js' + }); + map.addMapping({ + original: { line: 1, column: 1 }, + generated: { line: 2, column: 2 }, + source: 'bang.coffee' + }); + map.addMapping({ + original: { line: 5, column: 5 }, + generated: { line: 6, column: 6 }, + source: 'bang.coffee' + }); + map = new SourceMapConsumer(map.toString()); + + // Should handle without sourceRoot. + var pos = map.generatedPositionFor({ + line: 1, + column: 1, + source: 'bang.coffee' + }); + + assert.equal(pos.line, 2); + assert.equal(pos.column, 2); + + // Should handle with sourceRoot. + var pos = map.generatedPositionFor({ + line: 1, + column: 1, + source: 'foo/bar/bang.coffee' + }); + + assert.equal(pos.line, 2); + assert.equal(pos.column, 2); + }; + + exports['test sourceRoot + originalPositionFor'] = function (assert, util) { + var map = new SourceMapGenerator({ + sourceRoot: 'foo/bar', + file: 'baz.js' + }); + map.addMapping({ + original: { line: 1, column: 1 }, + generated: { line: 2, column: 2 }, + source: 'bang.coffee' + }); + map = new SourceMapConsumer(map.toString()); + + var pos = map.originalPositionFor({ + line: 2, + column: 2, + }); + + // Should always have the prepended source root + assert.equal(pos.source, 'foo/bar/bang.coffee'); + assert.equal(pos.line, 1); + assert.equal(pos.column, 1); + }; + + exports['test github issue #56'] = function (assert, util) { + var map = new SourceMapGenerator({ + sourceRoot: 'http://', + file: 'www.example.com/foo.js' + }); + map.addMapping({ + original: { line: 1, column: 1 }, + generated: { line: 2, column: 2 }, + source: 'www.example.com/original.js' + }); + map = new SourceMapConsumer(map.toString()); + + var sources = map.sources; + assert.equal(map.sources.length, 1); + assert.equal(map.sources[0], 'http://www.example.com/original.js'); + }; + }); function run_test() { runSourceMapTests('test/source-map/test-source-map-consumer', do_throw); diff --git a/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js b/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js index e585755e22d..4811438935d 100644 --- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js +++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js @@ -15,6 +15,9 @@ Components.utils.import('resource://test/Utils.jsm'); define("test/source-map/test-source-map-generator", ["require", "exports", "module"], function (require, exports, module) { var SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator; + var SourceMapConsumer = require('source-map/source-map-consumer').SourceMapConsumer; + var SourceNode = require('source-map/source-node').SourceNode; + var util = require('source-map/util'); exports['test some simple stuff'] = function (assert, util) { var map = new SourceMapGenerator({ @@ -176,19 +179,100 @@ define("test/source-map/test-source-map-generator", ["require", "exports", "modu map = JSON.parse(map.toString()); - assert.equal(map.version, 3); - assert.equal(map.file, 'min.js'); - assert.equal(map.names.length, 3); - assert.equal(map.names[0], 'bar'); - assert.equal(map.names[1], 'baz'); - assert.equal(map.names[2], 'n'); - assert.equal(map.sources.length, 2); - assert.equal(map.sources[0], 'one.js'); - assert.equal(map.sources[1], 'two.js'); - assert.equal(map.sourceRoot, '/the/root'); - assert.equal(map.mappings, 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'); + util.assertEqualMaps(assert, map, util.testMap); }; + exports['test that source content can be set'] = function (assert, util) { + var map = new SourceMapGenerator({ + file: 'min.js', + sourceRoot: '/the/root' + }); + map.addMapping({ + generated: { line: 1, column: 1 }, + original: { line: 1, column: 1 }, + source: 'one.js' + }); + map.addMapping({ + generated: { line: 2, column: 1 }, + original: { line: 1, column: 1 }, + source: 'two.js' + }); + map.setSourceContent('one.js', 'one file content'); + + map = JSON.parse(map.toString()); + assert.equal(map.sources[0], 'one.js'); + assert.equal(map.sources[1], 'two.js'); + assert.equal(map.sourcesContent[0], 'one file content'); + assert.equal(map.sourcesContent[1], null); + }; + + exports['test .fromSourceMap'] = function (assert, util) { + var map = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(util.testMap)); + util.assertEqualMaps(assert, map.toJSON(), util.testMap); + }; + + exports['test .fromSourceMap with sourcesContent'] = function (assert, util) { + var map = SourceMapGenerator.fromSourceMap( + new SourceMapConsumer(util.testMapWithSourcesContent)); + util.assertEqualMaps(assert, map.toJSON(), util.testMapWithSourcesContent); + }; + + exports['test applySourceMap'] = function (assert, util) { + var node = new SourceNode(null, null, null, [ + new SourceNode(2, 0, 'fileX', 'lineX2\n'), + 'genA1\n', + new SourceNode(2, 0, 'fileY', 'lineY2\n'), + 'genA2\n', + new SourceNode(1, 0, 'fileX', 'lineX1\n'), + 'genA3\n', + new SourceNode(1, 0, 'fileY', 'lineY1\n') + ]); + var mapStep1 = node.toStringWithSourceMap({ + file: 'fileA' + }).map; + mapStep1.setSourceContent('fileX', 'lineX1\nlineX2\n'); + mapStep1 = mapStep1.toJSON(); + + node = new SourceNode(null, null, null, [ + 'gen1\n', + new SourceNode(1, 0, 'fileA', 'lineA1\n'), + new SourceNode(2, 0, 'fileA', 'lineA2\n'), + new SourceNode(3, 0, 'fileA', 'lineA3\n'), + new SourceNode(4, 0, 'fileA', 'lineA4\n'), + new SourceNode(1, 0, 'fileB', 'lineB1\n'), + new SourceNode(2, 0, 'fileB', 'lineB2\n'), + 'gen2\n' + ]); + var mapStep2 = node.toStringWithSourceMap({ + file: 'fileGen' + }).map; + mapStep2.setSourceContent('fileB', 'lineB1\nlineB2\n'); + mapStep2 = mapStep2.toJSON(); + + node = new SourceNode(null, null, null, [ + 'gen1\n', + new SourceNode(2, 0, 'fileX', 'lineA1\n'), + new SourceNode(2, 0, 'fileA', 'lineA2\n'), + new SourceNode(2, 0, 'fileY', 'lineA3\n'), + new SourceNode(4, 0, 'fileA', 'lineA4\n'), + new SourceNode(1, 0, 'fileB', 'lineB1\n'), + new SourceNode(2, 0, 'fileB', 'lineB2\n'), + 'gen2\n' + ]); + var expectedMap = node.toStringWithSourceMap({ + file: 'fileGen' + }).map; + expectedMap.setSourceContent('fileX', 'lineX1\nlineX2\n'); + expectedMap.setSourceContent('fileB', 'lineB1\nlineB2\n'); + expectedMap = expectedMap.toJSON(); + + // apply source map "mapStep1" to "mapStep2" + var generator = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(mapStep2)); + generator.applySourceMap(new SourceMapConsumer(mapStep1)); + var actualMap = generator.toJSON(); + + util.assertEqualMaps(assert, actualMap, expectedMap); + }; }); function run_test() { runSourceMapTests('test/source-map/test-source-map-generator', do_throw); diff --git a/toolkit/devtools/sourcemap/tests/unit/test_source_node.js b/toolkit/devtools/sourcemap/tests/unit/test_source_node.js index afdbc781d9d..5b0163282a0 100644 --- a/toolkit/devtools/sourcemap/tests/unit/test_source_node.js +++ b/toolkit/devtools/sourcemap/tests/unit/test_source_node.js @@ -136,7 +136,10 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun exports['test .toStringWithSourceMap()'] = function (assert, util) { var node = new SourceNode(null, null, null, ['(function () {\n', - ' ', new SourceNode(1, 0, 'a.js', ['someCall()']), ';\n', + ' ', + new SourceNode(1, 0, 'a.js', 'someCall', 'originalCall'), + new SourceNode(1, 8, 'a.js', '()'), + ';\n', ' ', new SourceNode(2, 0, 'b.js', ['if (foo) bar()']), ';\n', '}());']); var map = node.toStringWithSourceMap({ @@ -148,6 +151,14 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun var actual; + actual = map.originalPositionFor({ + line: 1, + column: 4 + }); + assert.equal(actual.source, null); + assert.equal(actual.line, null); + assert.equal(actual.column, null); + actual = map.originalPositionFor({ line: 2, column: 2 @@ -155,6 +166,7 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun assert.equal(actual.source, 'a.js'); assert.equal(actual.line, 1); assert.equal(actual.column, 0); + assert.equal(actual.name, 'originalCall'); actual = map.originalPositionFor({ line: 3, @@ -163,8 +175,115 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun assert.equal(actual.source, 'b.js'); assert.equal(actual.line, 2); assert.equal(actual.column, 0); + + actual = map.originalPositionFor({ + line: 3, + column: 16 + }); + assert.equal(actual.source, null); + assert.equal(actual.line, null); + assert.equal(actual.column, null); + + actual = map.originalPositionFor({ + line: 4, + column: 2 + }); + assert.equal(actual.source, null); + assert.equal(actual.line, null); + assert.equal(actual.column, null); }; + exports['test .fromStringWithSourceMap()'] = function (assert, util) { + var node = SourceNode.fromStringWithSourceMap( + util.testGeneratedCode, + new SourceMapConsumer(util.testMap)); + + var result = node.toStringWithSourceMap({ + file: 'min.js' + }); + var map = result.map; + var code = result.code; + + assert.equal(code, util.testGeneratedCode); + assert.ok(map instanceof SourceMapGenerator, 'map instanceof SourceMapGenerator'); + map = map.toJSON(); + assert.equal(map.version, util.testMap.version); + assert.equal(map.file, util.testMap.file); + assert.equal(map.mappings, util.testMap.mappings); + }; + + exports['test .fromStringWithSourceMap() complex version'] = function (assert, util) { + var input = new SourceNode(null, null, null, [ + "(function() {\n", + " var Test = {};\n", + " ", new SourceNode(1, 0, "a.js", "Test.A = { value: 1234 };\n"), + " ", new SourceNode(2, 0, "a.js", "Test.A.x = 'xyz';"), "\n", + "}());\n", + "/* Generated Source */"]); + input = input.toStringWithSourceMap({ + file: 'foo.js' + }); + + var node = SourceNode.fromStringWithSourceMap( + input.code, + new SourceMapConsumer(input.map.toString())); + + var result = node.toStringWithSourceMap({ + file: 'foo.js' + }); + var map = result.map; + var code = result.code; + + assert.equal(code, input.code); + assert.ok(map instanceof SourceMapGenerator, 'map instanceof SourceMapGenerator'); + map = map.toJSON(); + var inputMap = input.map.toJSON(); + util.assertEqualMaps(assert, map, inputMap); + }; + + exports['test setSourceContent with toStringWithSourceMap'] = function (assert, util) { + var aNode = new SourceNode(1, 1, 'a.js', 'a'); + aNode.setSourceContent('a.js', 'someContent'); + var node = new SourceNode(null, null, null, + ['(function () {\n', + ' ', aNode, + ' ', new SourceNode(1, 1, 'b.js', 'b'), + '}());']); + node.setSourceContent('b.js', 'otherContent'); + var map = node.toStringWithSourceMap({ + file: 'foo.js' + }).map; + + assert.ok(map instanceof SourceMapGenerator, 'map instanceof SourceMapGenerator'); + map = new SourceMapConsumer(map.toString()); + + assert.equal(map.sources.length, 2); + assert.equal(map.sources[0], 'a.js'); + assert.equal(map.sources[1], 'b.js'); + assert.equal(map.sourcesContent.length, 2); + assert.equal(map.sourcesContent[0], 'someContent'); + assert.equal(map.sourcesContent[1], 'otherContent'); + }; + + exports['test walkSourceContents'] = function (assert, util) { + var aNode = new SourceNode(1, 1, 'a.js', 'a'); + aNode.setSourceContent('a.js', 'someContent'); + var node = new SourceNode(null, null, null, + ['(function () {\n', + ' ', aNode, + ' ', new SourceNode(1, 1, 'b.js', 'b'), + '}());']); + node.setSourceContent('b.js', 'otherContent'); + var results = []; + node.walkSourceContents(function (sourceFile, sourceContent) { + results.push([sourceFile, sourceContent]); + }); + assert.equal(results.length, 2); + assert.equal(results[0][0], 'a.js'); + assert.equal(results[0][1], 'someContent'); + assert.equal(results[1][0], 'b.js'); + assert.equal(results[1][1], 'otherContent'); + }; }); function run_test() { runSourceMapTests('test/source-map/test-source-node', do_throw); From 3bb0728fee66522ce37fa154a2f17866007570ee Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Tue, 9 Apr 2013 11:40:00 +0300 Subject: [PATCH 08/14] Bug 849069 - relative source map URLs should be resolved according to the spec's rules; r=past --- .../devtools/debugger/test/binary_search.js | 3 +- .../devtools/debugger/test/binary_search.map | 3 +- .../debugger/server/dbg-script-actors.js | 42 ++++++++++++---- .../devtools/debugger/tests/unit/head_dbg.js | 25 ++++++++++ .../unit/source-map-data/sourcemapped.coffee | 6 +++ .../unit/source-map-data/sourcemapped.map | 10 ++++ .../debugger/tests/unit/sourcemapped.js | 16 +++++++ .../debugger/tests/unit/test_source-01.js | 10 +--- .../debugger/tests/unit/test_sourcemaps-04.js | 48 +++++++++++++++++++ .../debugger/tests/unit/test_sourcemaps-05.js | 48 +++++++++++++++++++ .../devtools/debugger/tests/unit/xpcshell.ini | 6 +++ toolkit/devtools/sourcemap/SourceMap.jsm | 30 ++++++++++-- .../devtools/sourcemap/tests/unit/Utils.jsm | 48 ++++++++++++++++--- .../tests/unit/test_source_map_consumer.js | 42 +++++++++++++++- 14 files changed, 305 insertions(+), 32 deletions(-) create mode 100644 toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.coffee create mode 100644 toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.map create mode 100644 toolkit/devtools/debugger/tests/unit/sourcemapped.js create mode 100644 toolkit/devtools/debugger/tests/unit/test_sourcemaps-04.js create mode 100644 toolkit/devtools/debugger/tests/unit/test_sourcemaps-05.js diff --git a/browser/devtools/debugger/test/binary_search.js b/browser/devtools/debugger/test/binary_search.js index ce77903e1b0..aa3dff3bc82 100644 --- a/browser/devtools/debugger/test/binary_search.js +++ b/browser/devtools/debugger/test/binary_search.js @@ -24,7 +24,6 @@ }).call(this); -// TODO bug 849069: this should just be "binary_search.map", not a full path. /* -//@ sourceMappingURL=http://example.com/browser/browser/devtools/debugger/test/binary_search.map +//@ sourceMappingURL=binary_search.map */ diff --git a/browser/devtools/debugger/test/binary_search.map b/browser/devtools/debugger/test/binary_search.map index a86e74783f1..c5aaeab2f01 100644 --- a/browser/devtools/debugger/test/binary_search.map +++ b/browser/devtools/debugger/test/binary_search.map @@ -1,8 +1,9 @@ { "version": 3, "file": "binary_search.js", + "sourceRoot": "", "sources": [ - "http://example.com/browser/browser/devtools/debugger/test/binary_search.coffee" + "binary_search.coffee" ], "names": [], "mappings": ";AACA;CAAA;CAAA,CAAA,CAAuB,EAAA,CAAjB,GAAkB,IAAxB;CAEE,OAAA,UAAA;CAAA,EAAQ,CAAR,CAAA;CAAA,EACQ,CAAR,CAAa,CAAL;CADR,EAEQ,CAAR,CAAA;CAEA,EAA0C,CAAR,CAAtB,MAAN;CAGJ,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,CAAR,CAAQ,GAAR;QAAA;CACA,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,EAAR,GAAA;QADA;CAAA,EAIQ,CAAI,CAAZ,CAAA;CAXF,IAIA;CAUA,GAAA,CAAS;CAAT,YAA8B;MAA9B;AAA0C,CAAD,YAAA;MAhBpB;CAAvB,EAAuB;CAAvB" diff --git a/toolkit/devtools/debugger/server/dbg-script-actors.js b/toolkit/devtools/debugger/server/dbg-script-actors.js index d7fd8a061a9..efb59d4f1fb 100644 --- a/toolkit/devtools/debugger/server/dbg-script-actors.js +++ b/toolkit/devtools/debugger/server/dbg-script-actors.js @@ -525,7 +525,9 @@ ThreadActor.prototype = { originalLine) return locationPromise.then((aLocation) => { let line = aLocation.line; - if (this.dbg.findScripts({ url: aLocation.url }).length == 0 || line < 0) { + if (this.dbg.findScripts({ url: aLocation.url }).length == 0 || + line < 0 || + line == null) { return { error: "noScript" }; } @@ -2405,7 +2407,7 @@ ThreadSources.prototype = { ]; }, (e) => { reportError(e); - delete this._sourceMaps[aScript.sourceMapURL]; + delete this._sourceMaps[this._normalize(aScript.sourceMapURL, aScript.url)]; delete this._sourceMapsByGeneratedSource[aScript.url]; return [this.source(aScript.url)]; }) @@ -2422,7 +2424,9 @@ ThreadSources.prototype = { return this._sourceMapsByGeneratedSource[aScript.url]; } dbg_assert(aScript.sourceMapURL); - let map = this._fetchSourceMap(aScript.sourceMapURL) + let sourceMapURL = this._normalize(aScript.sourceMapURL, + aScript.url); + let map = this._fetchSourceMap(sourceMapURL) .then((aSourceMap) => { for (let s of aSourceMap.sources) { this._generatedUrlsByOriginalUrl[s] = aScript.url; @@ -2437,14 +2441,21 @@ ThreadSources.prototype = { /** * Fetch the source map located at the given url. */ - _fetchSourceMap: function TS__featchSourceMap(aSourceMapURL) { - if (aSourceMapURL in this._sourceMaps) { - return this._sourceMaps[aSourceMapURL]; + _fetchSourceMap: function TS__fetchSourceMap(aAbsSourceMapURL) { + if (aAbsSourceMapURL in this._sourceMaps) { + return this._sourceMaps[aAbsSourceMapURL]; } else { - let promise = fetch(aSourceMapURL).then(function (rawSourceMap) { - return new SourceMapConsumer(rawSourceMap); + let promise = fetch(aAbsSourceMapURL).then((rawSourceMap) => { + let map = new SourceMapConsumer(rawSourceMap); + let base = aAbsSourceMapURL.replace(/\/[^\/]+$/, '/'); + if (base.indexOf("data:") !== 0) { + map.sourceRoot = map.sourceRoot + ? this._normalize(map.sourceRoot, base) + : base; + } + return map; }); - this._sourceMaps[aSourceMapURL] = promise; + this._sourceMaps[aAbsSourceMapURL] = promise; return promise; } }, @@ -2507,6 +2518,19 @@ ThreadSources.prototype = { }); }, + /** + * Normalize multiple relative paths towards the base paths on the right. + */ + _normalize: function TS__normalize(...aURLs) { + dbg_assert(aURLs.length > 1); + let base = Services.io.newURI(aURLs.pop(), null, null); + let url; + while ((url = aURLs.pop())) { + base = Services.io.newURI(url, null, base); + } + return base.spec; + }, + iter: function TS_iter() { for (let url in this._sourceActors) { yield this._sourceActors[url]; diff --git a/toolkit/devtools/debugger/tests/unit/head_dbg.js b/toolkit/devtools/debugger/tests/unit/head_dbg.js index 525ae7e4655..69207ed9358 100644 --- a/toolkit/devtools/debugger/tests/unit/head_dbg.js +++ b/toolkit/devtools/debugger/tests/unit/head_dbg.js @@ -175,6 +175,14 @@ function finishClient(aClient) }); } +/** + * Takes a relative file path and returns the absolute file url for it. + */ +function getFileUrl(aName) { + let file = do_get_file(aName); + return Services.io.newFileURI(file).spec; +} + /** * Returns the full path of the file with the specified name in a * platform-independent and URL-like form. @@ -190,3 +198,20 @@ function getFilePath(aName) } return path.slice(filePrePath.length); } + +Cu.import("resource://gre/modules/NetUtil.jsm"); + +/** + * Returns the full text contents of the given file. + */ +function readFile(aFileName) { + let f = do_get_file(aFileName); + let s = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + s.init(f, -1, -1, false); + try { + return NetUtil.readInputStreamToString(s, s.available()); + } finally { + s.close(); + } +} diff --git a/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.coffee b/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.coffee new file mode 100644 index 00000000000..73a400a219d --- /dev/null +++ b/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.coffee @@ -0,0 +1,6 @@ +foo = (n) -> + return "foo" + i for i in [0...n] + +[first, second, third] = foo(3) + +debugger \ No newline at end of file diff --git a/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.map b/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.map new file mode 100644 index 00000000000..dcee3c33c39 --- /dev/null +++ b/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "file": "sourcemapped.js", + "sourceRoot": "", + "sources": [ + "sourcemapped.coffee" + ], + "names": [], + "mappings": ";AAAA;CAAA,KAAA,yBAAA;CAAA;CAAA,CAAA,CAAA,MAAO;CACL,IAAA,GAAA;AAAA,CAAA,EAAA,MAA0B,qDAA1B;CAAA,EAAe,EAAR,QAAA;CAAP,IADI;CAAN,EAAM;;CAAN,CAGA,CAAyB,IAAA;;CAEzB,UALA;CAAA" +} \ No newline at end of file diff --git a/toolkit/devtools/debugger/tests/unit/sourcemapped.js b/toolkit/devtools/debugger/tests/unit/sourcemapped.js new file mode 100644 index 00000000000..7dbcb2d1493 --- /dev/null +++ b/toolkit/devtools/debugger/tests/unit/sourcemapped.js @@ -0,0 +1,16 @@ +// Generated by CoffeeScript 1.6.1 +(function() { + var first, foo, second, third, _ref; + + foo = function(n) { + var i, _i; + for (i = _i = 0; 0 <= n ? _i < n : _i > n; i = 0 <= n ? ++_i : --_i) { + return "foo" + i; + } + }; + + _ref = foo(3), first = _ref[0], second = _ref[1], third = _ref[2]; + + debugger; + +}).call(this); diff --git a/toolkit/devtools/debugger/tests/unit/test_source-01.js b/toolkit/devtools/debugger/tests/unit/test_source-01.js index a1fd94e4c48..684e38f44d8 100644 --- a/toolkit/devtools/debugger/tests/unit/test_source-01.js +++ b/toolkit/devtools/debugger/tests/unit/test_source-01.js @@ -10,8 +10,6 @@ var gThreadClient; // and that they can communicate over the protocol to fetch the source text for // a given script. -Cu.import("resource://gre/modules/NetUtil.jsm"); - function run_test() { initTestDebuggerServer(); @@ -60,15 +58,9 @@ function test_source() do_check_true(!aResponse.error); do_check_true(!!aResponse.source); - let f = do_get_file("test_source-01.js", false); - let s = Cc["@mozilla.org/network/file-input-stream;1"] - .createInstance(Ci.nsIFileInputStream); - s.init(f, -1, -1, false); - - do_check_eq(NetUtil.readInputStreamToString(s, s.available()), + do_check_eq(readFile("test_source-01.js"), aResponse.source); - s.close(); gThreadClient.resume(function () { finishClient(gClient); }); diff --git a/toolkit/devtools/debugger/tests/unit/test_sourcemaps-04.js b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-04.js new file mode 100644 index 00000000000..af61f6c4896 --- /dev/null +++ b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-04.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that absolute source map urls work. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +Components.utils.import('resource:///modules/devtools/SourceMap.jsm'); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect(function() { + attachTestGlobalClientAndResume(gClient, "test-source-map", function(aResponse, aThreadClient) { + gThreadClient = aThreadClient; + test_absolute_source_map(); + }); + }); + do_test_pending(); +} + +function test_absolute_source_map() +{ + gClient.addOneTimeListener("newSource", function _onNewSource(aEvent, aPacket) { + do_check_eq(aEvent, "newSource"); + do_check_eq(aPacket.type, "newSource"); + do_check_true(!!aPacket.source); + + do_check_true(aPacket.source.url.indexOf("sourcemapped.coffee") !== -1, + "The new source should be a coffee file."); + do_check_eq(aPacket.source.url.indexOf("sourcemapped.js"), -1, + "The new source should not be a js file."); + + finishClient(gClient); + }); + + code = readFile("sourcemapped.js") + + "\n//@ sourceMappingURL=" + getFileUrl("source-map-data/sourcemapped.map"); + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + getFileUrl("sourcemapped.js"), 1); +} diff --git a/toolkit/devtools/debugger/tests/unit/test_sourcemaps-05.js b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-05.js new file mode 100644 index 00000000000..352fab41543 --- /dev/null +++ b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-05.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Check that relative source map urls work. + */ + +var gDebuggee; +var gClient; +var gThreadClient; + +Components.utils.import('resource:///modules/devtools/SourceMap.jsm'); + +function run_test() +{ + initTestDebuggerServer(); + gDebuggee = addTestGlobal("test-source-map"); + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect(function() { + attachTestGlobalClientAndResume(gClient, "test-source-map", function(aResponse, aThreadClient) { + gThreadClient = aThreadClient; + test_relative_source_map(); + }); + }); + do_test_pending(); +} + +function test_relative_source_map() +{ + gClient.addOneTimeListener("newSource", function _onNewSource(aEvent, aPacket) { + do_check_eq(aEvent, "newSource"); + do_check_eq(aPacket.type, "newSource"); + do_check_true(!!aPacket.source); + + do_check_true(aPacket.source.url.indexOf("sourcemapped.coffee") !== -1, + "The new source should be a coffee file."); + do_check_eq(aPacket.source.url.indexOf("sourcemapped.js"), -1, + "The new source should not be a js file."); + + finishClient(gClient); + }); + + code = readFile("sourcemapped.js") + + "\n//@ sourceMappingURL=source-map-data/sourcemapped.map"; + + Components.utils.evalInSandbox(code, gDebuggee, "1.8", + getFileUrl("sourcemapped.js"), 1); +} diff --git a/toolkit/devtools/debugger/tests/unit/xpcshell.ini b/toolkit/devtools/debugger/tests/unit/xpcshell.ini index bdf4d89ad35..a273cdff594 100644 --- a/toolkit/devtools/debugger/tests/unit/xpcshell.ini +++ b/toolkit/devtools/debugger/tests/unit/xpcshell.ini @@ -81,6 +81,12 @@ reason = bug 820380 [test_sourcemaps-01.js] [test_sourcemaps-02.js] [test_sourcemaps-03.js] +[test_sourcemaps-04.js] +skip-if = toolkit == "gonk" +reason = bug 820380 +[test_sourcemaps-05.js] +skip-if = toolkit == "gonk" +reason = bug 820380 [test_objectgrips-01.js] [test_objectgrips-02.js] [test_objectgrips-03.js] diff --git a/toolkit/devtools/sourcemap/SourceMap.jsm b/toolkit/devtools/sourcemap/SourceMap.jsm index b8f9bd4258a..05139f7969d 100644 --- a/toolkit/devtools/sourcemap/SourceMap.jsm +++ b/toolkit/devtools/sourcemap/SourceMap.jsm @@ -470,10 +470,34 @@ define('source-map/util', ['require', 'exports', 'module' , ], function(require, } exports.getArg = getArg; + var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/; + + function urlParse(aUrl) { + var match = aUrl.match(urlRegexp); + if (!match) { + return null; + } + return { + scheme: match[1], + auth: match[3], + host: match[4], + port: match[6], + path: match[7] + }; + } + function join(aRoot, aPath) { - return aPath.charAt(0) === '/' - ? aPath - : aRoot.replace(/\/$/, '') + '/' + aPath; + var url; + + if (aPath.match(urlRegexp)) { + return aPath; + } + + if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) { + return aRoot.replace(url.path, '') + aPath; + } + + return aRoot.replace(/\/$/, '') + '/' + aPath; } exports.join = join; diff --git a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm index ceb12e0141d..d78371f3757 100644 --- a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm +++ b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm @@ -145,9 +145,21 @@ define('test/source-map/util', ['require', 'exports', 'module' , 'lib/source-ma assert.equal(origMapping.column, originalColumn, 'Incorrect column, expected ' + JSON.stringify(originalColumn) + ', got ' + JSON.stringify(origMapping.column)); - assert.equal(origMapping.source, - originalSource ? util.join(map._sourceRoot, originalSource) : null, - 'Incorrect source, expected ' + JSON.stringify(originalSource) + + var expectedSource; + + if (originalSource && map.sourceRoot && originalSource.indexOf(map.sourceRoot) === 0) { + expectedSource = originalSource; + } else if (originalSource) { + expectedSource = map.sourceRoot + ? util.join(map.sourceRoot, originalSource) + : originalSource; + } else { + expectedSource = null; + } + + assert.equal(origMapping.source, expectedSource, + 'Incorrect source, expected ' + JSON.stringify(expectedSource) + ', got ' + JSON.stringify(origMapping.source)); } @@ -238,10 +250,34 @@ define('lib/source-map/util', ['require', 'exports', 'module' , ], function(requ } exports.getArg = getArg; + var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/; + + function urlParse(aUrl) { + var match = aUrl.match(urlRegexp); + if (!match) { + return null; + } + return { + scheme: match[1], + auth: match[3], + host: match[4], + port: match[6], + path: match[7] + }; + } + function join(aRoot, aPath) { - return aPath.charAt(0) === '/' - ? aPath - : aRoot.replace(/\/$/, '') + '/' + aPath; + var url; + + if (aPath.match(urlRegexp)) { + return aPath; + } + + if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) { + return aRoot.replace(url.path, '') + aPath; + } + + return aRoot.replace(/\/$/, '') + '/' + aPath; } exports.join = join; diff --git a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js index 3b7a59e7e11..515287004df 100644 --- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js +++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js @@ -251,8 +251,46 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul map = new SourceMapConsumer(map.toString()); var sources = map.sources; - assert.equal(map.sources.length, 1); - assert.equal(map.sources[0], 'http://www.example.com/original.js'); + assert.equal(sources.length, 1); + assert.equal(sources[0], 'http://www.example.com/original.js'); + }; + + exports['test github issue #43'] = function (assert, util) { + var map = new SourceMapGenerator({ + sourceRoot: 'http://example.com', + file: 'foo.js' + }); + map.addMapping({ + original: { line: 1, column: 1 }, + generated: { line: 2, column: 2 }, + source: 'http://cdn.example.com/original.js' + }); + map = new SourceMapConsumer(map.toString()); + + var sources = map.sources; + assert.equal(sources.length, 1, + 'Should only be one source.'); + assert.equal(sources[0], 'http://cdn.example.com/original.js', + 'Should not be joined with the sourceRoot.'); + }; + + exports['test absolute path, but same host sources'] = function (assert, util) { + var map = new SourceMapGenerator({ + sourceRoot: 'http://example.com/foo/bar', + file: 'foo.js' + }); + map.addMapping({ + original: { line: 1, column: 1 }, + generated: { line: 2, column: 2 }, + source: '/original.js' + }); + map = new SourceMapConsumer(map.toString()); + + var sources = map.sources; + assert.equal(sources.length, 1, + 'Should only be one source.'); + assert.equal(sources[0], 'http://example.com/original.js', + 'Source should be relative the host of the source root.'); }; }); From 9ec99ed2dd48f51166c8586adb51f8a0bbe33669 Mon Sep 17 00:00:00 2001 From: Reuben Morais Date: Tue, 16 Apr 2013 14:01:13 +0200 Subject: [PATCH 09/14] Bug 862247 - Fix Contacts/Settings tests not running individually. r=gwagner --HG-- extra : rebase_source : c9048a4680383576f329c4dcfb139a8e05633076 --- dom/contacts/tests/test_contacts_basics.html | 6 +++--- dom/contacts/tests/test_contacts_blobs.html | 16 ++++++++++------ dom/contacts/tests/test_contacts_getall.html | 9 +++++---- .../tests/test_contacts_international.html | 15 ++++++++++----- dom/settings/tests/test_settings_blobs.html | 7 ++++++- .../tests/test_settings_onsettingchange.html | 13 +++++++++---- 6 files changed, 43 insertions(+), 23 deletions(-) diff --git a/dom/contacts/tests/test_contacts_basics.html b/dom/contacts/tests/test_contacts_basics.html index 2e3fd21dec6..8393efda273 100644 --- a/dom/contacts/tests/test_contacts_basics.html +++ b/dom/contacts/tests/test_contacts_basics.html @@ -1,17 +1,17 @@ - Test for Bug {674720} WebContacts + Test for Bug 674720 WebContacts -Mozilla Bug {674720} +Mozilla Bug 674720