diff --git a/browser/devtools/webconsole/HUDService-content.js b/browser/devtools/webconsole/HUDService-content.js index 1648a388bf3..cbc046f6d11 100644 --- a/browser/devtools/webconsole/HUDService-content.js +++ b/browser/devtools/webconsole/HUDService-content.js @@ -499,9 +499,8 @@ let Manager = { Manager = ConsoleAPIObserver = JSTerm = ConsoleListener = NetworkMonitor = NetworkResponseListener = ConsoleProgressListener = null; - Cc = Ci = Cu = XPCOMUtils = Services = gConsoleStorage = - WebConsoleUtils = l10n = JSPropertyProvider = NetworkHelper = - NetUtil = activityDistributor = null; + XPCOMUtils = gConsoleStorage = WebConsoleUtils = l10n = JSPropertyProvider = + NetworkHelper = NetUtil = activityDistributor = null; }, }; @@ -1503,7 +1502,7 @@ NetworkResponseListener.prototype = { */ _findOpenResponse: function NRL__findOpenResponse() { - if (this._foundOpenResponse) { + if (!_alive || this._foundOpenResponse) { return; } @@ -1611,7 +1610,9 @@ NetworkResponseListener.prototype = { this.receivedData = ""; - NetworkMonitor.sendActivity(this.httpActivity); + if (_alive) { + NetworkMonitor.sendActivity(this.httpActivity); + } this.httpActivity.channel = null; this.httpActivity = null; @@ -1745,7 +1746,7 @@ let NetworkMonitor = { // NetworkResponseListener is responsible with updating the httpActivity // object with the data from the new object in openResponses. - if (aTopic != "http-on-examine-response" || + if (!_alive || aTopic != "http-on-examine-response" || !(aSubject instanceof Ci.nsIHttpChannel)) { return; } diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index eefdf987f56..2a7b97bca86 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -173,6 +173,14 @@ const GROUP_INDENT = 12; // The pref prefix for webconsole filters const PREFS_PREFIX = "devtools.webconsole.filter."; +// The number of messages to display in a single display update. If we display +// too many messages at once we slow the Firefox UI too much. +const MESSAGES_IN_INTERVAL = 30; + +// The delay between display updates - tells how often we should push new +// messages to screen. +const OUTPUT_INTERVAL = 90; // milliseconds + /////////////////////////////////////////////////////////////////////////// //// Helper for creating the network panel. @@ -212,58 +220,21 @@ function createElement(aDocument, aTag, aAttributes) * @param integer aCategory * The category of message nodes to limit. * @return number - * The current user-selected log limit. + * The number of removed nodes. */ function pruneConsoleOutputIfNecessary(aHUDId, aCategory) { - // Get the log limit, either from the pref or from the constant. - let logLimit; - try { - let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory]; - logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName); - } catch (e) { - logLimit = DEFAULT_LOG_LIMIT; - } - let hudRef = HUDService.getHudReferenceById(aHUDId); let outputNode = hudRef.outputNode; + let logLimit = hudRef.logLimitForCategory(aCategory); - let scrollBox = outputNode.scrollBoxObject.element; - let oldScrollHeight = scrollBox.scrollHeight; - let scrolledToBottom = ConsoleUtils.isOutputScrolledToBottom(outputNode); - - // Prune the nodes. - let messageNodes = outputNode.querySelectorAll(".webconsole-msg-" + + let messageNodes = outputNode.getElementsByClassName("webconsole-msg-" + CATEGORY_CLASS_FRAGMENTS[aCategory]); - let removeNodes = messageNodes.length - logLimit; - for (let i = 0; i < removeNodes; i++) { - let node = messageNodes[i]; - if (node._evalCacheId && !node._panelOpen) { - hudRef.jsterm.clearObjectCache(node._evalCacheId); - } + let n = Math.max(0, messageNodes.length - logLimit); + let toRemove = Array.prototype.slice.call(messageNodes, 0, n); + toRemove.forEach(hudRef.removeOutputMessage, hudRef); - if (node.classList.contains("webconsole-msg-cssparser")) { - let desc = messageNodes[i].childNodes[2].textContent; - let location = ""; - if (node.childNodes[4]) { - location = node.childNodes[4].getAttribute("title"); - } - delete hudRef.cssNodes[desc + location]; - } - else if (node.classList.contains("webconsole-msg-inspector")) { - hudRef.pruneConsoleDirNode(node); - continue; - } - - node.parentNode.removeChild(node); - } - - if (!scrolledToBottom && removeNodes > 0 && - oldScrollHeight != scrollBox.scrollHeight) { - scrollBox.scrollTop -= oldScrollHeight - scrollBox.scrollHeight; - } - - return logLimit; + return n; } /////////////////////////////////////////////////////////////////////////// @@ -470,11 +441,10 @@ HUD_SERVICE.prototype = { // Go through the nodes and adjust the placement of "webconsole-new-group" // classes. - let nodes = aOutputNode.querySelectorAll(".hud-msg-node" + ":not(.hud-filtered-by-string):not(.hud-filtered-by-type)"); let lastTimestamp; - for (let i = 0; i < nodes.length; i++) { + for (let i = 0, n = nodes.length; i < n; i++) { let thisTimestamp = nodes[i].timestamp; if (lastTimestamp != null && thisTimestamp >= lastTimestamp + NEW_GROUP_DELAY) { @@ -565,9 +535,9 @@ HUD_SERVICE.prototype = { let outputNode = this.getHudReferenceById(aHUDId).outputNode; - let nodes = outputNode.querySelectorAll(".hud-msg-node"); + let nodes = outputNode.getElementsByClassName("hud-msg-node"); - for (let i = 0; i < nodes.length; ++i) { + for (let i = 0, n = nodes.length; i < n; ++i) { let node = nodes[i]; // hide nodes that match the strings @@ -1054,6 +1024,9 @@ function HeadsUpDisplay(aTab) // create a panel dynamically and attach to the parentNode this.createHUD(); + this._outputQueue = []; + this._pruneCategoriesQueue = {}; + // create the JSTerm input element this.jsterm = new JSTerm(this); this.jsterm.inputNode.focus(); @@ -1065,6 +1038,40 @@ function HeadsUpDisplay(aTab) } HeadsUpDisplay.prototype = { + /** + * Last time when we displayed any message in the output. Timestamp in + * milliseconds since the Unix epoch. + * + * @private + * @type number + */ + _lastOutputFlush: 0, + + /** + * The number of messages displayed in the last interval. The interval is + * given by OUTPUT_INTERVAL. + * + * @private + * @type number + */ + _messagesDisplayedInInterval: 0, + + /** + * Message nodes are stored here in a queue for later display. + * + * @private + * @type array + */ + _outputQueue: null, + + /** + * Keep track of the categories we need to prune from time to time. + * + * @private + * @type array + */ + _pruneCategoriesQueue: null, + /** * Message names that the HUD listens for. These messages come from the remote * Web Console content script. @@ -1394,10 +1401,6 @@ HeadsUpDisplay.prototype = { return; } - // Turn off scrolling for the moment. - ConsoleUtils.scroll = false; - this.outputNode.hidden = true; - aRemoteMessages.forEach(function(aMessage) { switch (aMessage._type) { case "PageError": @@ -1408,17 +1411,6 @@ HeadsUpDisplay.prototype = { break; } }, this); - - this.outputNode.hidden = false; - ConsoleUtils.scroll = true; - - // Scroll to bottom. - let numChildren = this.outputNode.childNodes.length; - if (numChildren && this.outputNode.clientHeight) { - // We also check the clientHeight to force a reflow, otherwise - // ensureIndexIsVisible() does not work after outputNode.hidden = false. - this.outputNode.ensureIndexIsVisible(numChildren - 1); - } }, /** @@ -2485,12 +2477,273 @@ HeadsUpDisplay.prototype = { }, false); }, + /** + * Output a message node. This filters a node appropriately, then sends it to + * the output, regrouping and pruning output as necessary. + * + * Note: this call is async - the given message node may not be displayed when + * you call this method. + * + * @param nsIDOMNode aNode + * The message node to send to the output. + * @param nsIDOMNode [aNodeAfter] + * Insert the node after the given aNodeAfter (optional). + */ + outputMessageNode: function HUD_outputMessageNode(aNode, aNodeAfter) + { + this._outputQueue.push([aNode, aNodeAfter]); + this._flushMessageQueue(); + }, + + /** + * Try to flush the output message queue. This takes the messages in the + * output queue and displays them. Outputting stops at MESSAGES_IN_INTERVAL. + * Further output is queued to happen later - see OUTPUT_INTERVAL. + * + * @private + */ + _flushMessageQueue: function HUD__flushMessageQueue() + { + if ((Date.now() - this._lastOutputFlush) >= OUTPUT_INTERVAL) { + this._messagesDisplayedInInterval = 0; + } + + // Determine how many messages we can display now. + let toDisplay = Math.min(this._outputQueue.length, + MESSAGES_IN_INTERVAL - + this._messagesDisplayedInInterval); + + if (!toDisplay) { + if (!this._outputTimeout && this._outputQueue.length > 0) { + this._outputTimeout = + this.chromeWindow.setTimeout(function() { + delete this._outputTimeout; + this._flushMessageQueue(); + }.bind(this), OUTPUT_INTERVAL); + } + return; + } + + // Try to prune the message queue. + let shouldPrune = false; + if (this._outputQueue.length > toDisplay && this._pruneOutputQueue()) { + toDisplay = Math.min(this._outputQueue.length, toDisplay); + shouldPrune = true; + } + + let batch = this._outputQueue.splice(0, toDisplay); + if (!batch.length) { + return; + } + + let outputNode = this.outputNode; + let lastVisibleNode = null; + let scrolledToBottom = ConsoleUtils.isOutputScrolledToBottom(outputNode); + let scrollBox = outputNode.scrollBoxObject.element; + + let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId); + + // Output the current batch of messages. + for (let item of batch) { + if (this._outputMessageFromQueue(hudIdSupportsString, item)) { + lastVisibleNode = item[0]; + } + } + + // Keep track of how many messages we displayed, so we do not display too + // many at once. + this._messagesDisplayedInInterval += batch.length; + + let oldScrollHeight = 0; + + // Prune messages if needed. We do not do this for every flush call to + // improve performance. + let removedNodes = 0; + if (shouldPrune || !(this._outputQueue.length % 20)) { + oldScrollHeight = scrollBox.scrollHeight; + + let categories = Object.keys(this._pruneCategoriesQueue); + categories.forEach(function _pruneOutput(aCategory) { + removedNodes += pruneConsoleOutputIfNecessary(this.hudId, aCategory); + }, this); + this._pruneCategoriesQueue = {}; + } + + // Regroup messages at the end of the queue. + if (!this._outputQueue.length) { + HUDService.regroupOutput(outputNode); + } + + let isInputOutput = lastVisibleNode && + (lastVisibleNode.classList.contains("webconsole-msg-input") || + lastVisibleNode.classList.contains("webconsole-msg-output")); + + // Scroll to the new node if it is not filtered, and if the output node is + // scrolled at the bottom or if the new node is a jsterm input/output + // message. + if (lastVisibleNode && (scrolledToBottom || isInputOutput)) { + ConsoleUtils.scrollToVisible(lastVisibleNode); + } + else if (!scrolledToBottom && removedNodes > 0 && + oldScrollHeight != scrollBox.scrollHeight) { + // If there were pruned messages and if scroll is not at the bottom, then + // we need to adjust the scroll location. + scrollBox.scrollTop -= oldScrollHeight - scrollBox.scrollHeight; + } + + // If the queue is not empty, schedule another flush. + if (!this._outputTimeout && this._outputQueue.length > 0) { + this._outputTimeout = + this.chromeWindow.setTimeout(function() { + delete this._outputTimeout; + this._flushMessageQueue(); + }.bind(this), OUTPUT_INTERVAL); + } + + this._lastOutputFlush = Date.now(); + }, + + /** + * Output a message from the queue. + * + * @private + * @param nsISupportsString aHudIdSupportsString + * The HUD ID as an nsISupportsString. + * @param array aItem + * An item from the output queue - this item represents a message. + * @return boolean + * True if the message is visible, false otherwise. + */ + _outputMessageFromQueue: + function HUD__outputMessageFromQueue(aHudIdSupportsString, aItem) + { + let [node, afterNode] = aItem; + + let isFiltered = ConsoleUtils.filterMessageNode(node, this.hudId); + + let isRepeated = false; + if (node.classList.contains("webconsole-msg-cssparser")) { + isRepeated = ConsoleUtils.filterRepeatedCSS(node, this.outputNode, + this.hudId); + } + + if (!isRepeated && + (node.classList.contains("webconsole-msg-console") || + node.classList.contains("webconsole-msg-exception") || + node.classList.contains("webconsole-msg-error"))) { + isRepeated = ConsoleUtils.filterRepeatedConsole(node, this.outputNode); + } + + if (!isRepeated) { + this.outputNode.insertBefore(node, + afterNode ? afterNode.nextSibling : null); + this._pruneCategoriesQueue[node.category] = true; + } + + let nodeID = node.getAttribute("id"); + Services.obs.notifyObservers(aHudIdSupportsString, + "web-console-message-created", nodeID); + + return !isRepeated && !isFiltered; + }, + + /** + * Prune the queue of messages to display. This avoids displaying messages + * that will be removed at the end of the queue anyway. + * @private + */ + _pruneOutputQueue: function HUD__pruneOutputQueue() + { + let nodes = {}; + + // Group the messages per category. + this._outputQueue.forEach(function(aItem, aIndex) { + let [node] = aItem; + let category = node.category; + if (!(category in nodes)) { + nodes[category] = []; + } + nodes[category].push(aIndex); + }, this); + + let pruned = 0; + + // Loop through the categories we found and prune if needed. + for (let category in nodes) { + let limit = this.logLimitForCategory(category); + let indexes = nodes[category]; + if (indexes.length > limit) { + let n = Math.max(0, indexes.length - limit); + pruned += n; + for (let i = n - 1; i >= 0; i--) { + let node = this._outputQueue[indexes[i]][0]; + this._outputQueue.splice(indexes[i], 1); + } + } + } + + return pruned; + }, + + /** + * Retrieve the limit of messages for a specific category. + * + * @param number aCategory + * The category of messages you want to retrieve the limit for. See the + * CATEGORY_* constants. + * @return number + * The number of messages allowed for the specific category. + */ + logLimitForCategory: function HUD_logLimitForCategory(aCategory) + { + let logLimit = DEFAULT_LOG_LIMIT; + + try { + let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory]; + logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName); + logLimit = Math.max(logLimit, 1); + } + catch (e) { } + + return logLimit; + }, + + /** + * Remove a given message from the output. + * + * @param nsIDOMNode aNode + * The message node you want to remove. + */ + removeOutputMessage: function HUD_removeOutputMessage(aNode) + { + if (aNode._evalCacheId && !aNode._panelOpen) { + this.jsterm.clearObjectCache(aNode._evalCacheId); + } + + if (aNode.classList.contains("webconsole-msg-cssparser")) { + let desc = aNode.childNodes[2].textContent; + let location = ""; + if (aNode.childNodes[4]) { + location = aNode.childNodes[4].getAttribute("title"); + } + delete this.cssNodes[desc + location]; + } + else if (aNode.classList.contains("webconsole-msg-inspector")) { + this.pruneConsoleDirNode(aNode); + return; + } + + aNode.parentNode.removeChild(aNode); + }, + /** * Destroy the HUD object. Call this method to avoid memory leaks when the Web * Console is closed. */ destroy: function HUD_destroy() { + this._outputQueue = []; + this.sendMessageToContent("WebConsole:Destroy", {}); this._messageListeners.forEach(function(aName) { @@ -2533,6 +2786,8 @@ HeadsUpDisplay.prototype = { delete this.messageManager; delete this.browser; delete this.chromeDocument; + delete this.chromeWindow; + delete this.outputNode; this.positionMenuitems.above.removeEventListener("command", this._positionConsoleAbove, false); @@ -2592,7 +2847,7 @@ function JSTerm(aHud) this.hudId = this.hud.hudId; - this.lastCompletion = {}; + this.lastCompletion = { value: null }; this.history = []; this.historyIndex = 0; this.historyPlaceHolder = 0; // this.history.length; @@ -2890,17 +3145,7 @@ JSTerm.prototype = { let outputNode = hud.outputNode; let node; while ((node = outputNode.firstChild)) { - if (node._evalCacheId && !node._panelOpen) { - this.clearObjectCache(node._evalCacheId); - } - - if (node.classList && - node.classList.contains("webconsole-msg-inspector")) { - hud.pruneConsoleDirNode(node); - } - else { - outputNode.removeChild(node); - } + hud.removeOutputMessage(node); } hud.HUDBox.lastTimestamp = 0; @@ -3244,7 +3489,11 @@ JSTerm.prototype = { input: this.inputNode.value, }; - this.lastCompletion = {requestId: message.id, completionType: aType}; + this.lastCompletion = { + requestId: message.id, + completionType: aType, + value: null, + }; let callback = this._receiveAutocompleteProperties.bind(this, aCallback); this.hud.sendMessageToContent("JSTerm:Autocomplete", message, callback); }, @@ -3336,7 +3585,7 @@ JSTerm.prototype = { clearCompletion: function JSTF_clearCompletion() { this.autocompletePopup.clearItems(); - this.lastCompletion = {}; + this.lastCompletion = { value: null }; this.updateCompleteNode(""); if (this.autocompletePopup.isOpen) { this.autocompletePopup.hidePopup(); @@ -3389,7 +3638,10 @@ JSTerm.prototype = { */ clearObjectCache: function JST_clearObjectCache(aCacheId) { - this.hud.sendMessageToContent("JSTerm:ClearObjectCache", {cacheId: aCacheId}); + if (this.hud) { + this.hud.sendMessageToContent("JSTerm:ClearObjectCache", + { cacheId: aCacheId }); + } }, /** @@ -3887,13 +4139,18 @@ ConsoleUtils = { * The newly-created message node. * @param string aHUDId * The ID of the HUD which this node is to be inserted into. + * @return boolean + * True if the message was filtered or false otherwise. */ filterMessageNode: function ConsoleUtils_filterMessageNode(aNode, aHUDId) { + let isFiltered = false; + // Filter by the message type. let prefKey = MESSAGE_PREFERENCE_KEYS[aNode.category][aNode.severity]; if (prefKey && !HUDService.getFilterState(aHUDId, prefKey)) { // The node is filtered by type. aNode.classList.add("hud-filtered-by-type"); + isFiltered = true; } // Filter on the search string. @@ -3903,7 +4160,10 @@ ConsoleUtils = { // if string matches the filter text if (!HUDService.stringMatchesFilters(text, search)) { aNode.classList.add("hud-filtered-by-string"); + isFiltered = true; } + + return isFiltered; }, /** @@ -4010,50 +4270,8 @@ ConsoleUtils = { */ outputMessageNode: function ConsoleUtils_outputMessageNode(aNode, aHUDId, aNodeAfter) { - ConsoleUtils.filterMessageNode(aNode, aHUDId); - let outputNode = HUDService.hudReferences[aHUDId].outputNode; - - let scrolledToBottom = ConsoleUtils.isOutputScrolledToBottom(outputNode); - - let isRepeated = false; - if (aNode.classList.contains("webconsole-msg-cssparser")) { - isRepeated = this.filterRepeatedCSS(aNode, outputNode, aHUDId); - } - - if (!isRepeated && - (aNode.classList.contains("webconsole-msg-console") || - aNode.classList.contains("webconsole-msg-exception") || - aNode.classList.contains("webconsole-msg-error"))) { - isRepeated = this.filterRepeatedConsole(aNode, outputNode); - } - - if (!isRepeated) { - outputNode.insertBefore(aNode, aNodeAfter ? aNodeAfter.nextSibling : null); - } - - HUDService.regroupOutput(outputNode); - - if (pruneConsoleOutputIfNecessary(aHUDId, aNode.category) == 0) { - // We can't very well scroll to make the message node visible if the log - // limit is zero and the node was destroyed in the first place. - return; - } - - let isInputOutput = aNode.classList.contains("webconsole-msg-input") || - aNode.classList.contains("webconsole-msg-output"); - let isFiltered = aNode.classList.contains("hud-filtered-by-string") || - aNode.classList.contains("hud-filtered-by-type"); - - // Scroll to the new node if it is not filtered, and if the output node is - // scrolled at the bottom or if the new node is a jsterm input/output - // message. - if (!isFiltered && !isRepeated && (scrolledToBottom || isInputOutput)) { - ConsoleUtils.scrollToVisible(aNode); - } - - let id = WebConsoleUtils.supportsString(aHUDId); - let nodeID = aNode.getAttribute("id"); - Services.obs.notifyObservers(id, "web-console-message-created", nodeID); + let hud = HUDService.getHudReferenceById(aHUDId); + hud.outputMessageNode(aNode, aNodeAfter); }, /** @@ -4083,6 +4301,10 @@ ConsoleUtils = { HeadsUpDisplayUICommands = { refreshCommand: function UIC_refreshCommand() { var window = HUDService.currentContext(); + if (!window) { + return; + } + let command = window.document.getElementById("Tools:WebConsole"); if (this.getOpenHUD() != null) { command.setAttribute("checked", true); diff --git a/browser/devtools/webconsole/WebConsoleUtils.jsm b/browser/devtools/webconsole/WebConsoleUtils.jsm index d2bd321b792..d124ef2b854 100644 --- a/browser/devtools/webconsole/WebConsoleUtils.jsm +++ b/browser/devtools/webconsole/WebConsoleUtils.jsm @@ -888,6 +888,9 @@ function JSPropertyProvider(aScope, aInputValue) matchProp = properties.pop().trimLeft(); for (let i = 0; i < properties.length; i++) { let prop = properties[i].trim(); + if (!prop) { + return null; + } // If obj is undefined or null, then there is no chance to run completion // on it. Exit here. diff --git a/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js b/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js index 328d3300639..c35cbd7d53a 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js +++ b/browser/devtools/webconsole/test/browser_webconsole_basic_net_logging.js @@ -14,25 +14,32 @@ function test() { } function onLoad(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - openConsole(); - - browser.addEventListener("load", testBasicNetLogging, true); - content.location = TEST_NETWORK_URI; -} - -function testBasicNetLogging(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - - outputNode = HUDService.getHudByWindow(content).outputNode; - - executeSoon(function() { - findLogEntry("test-network.html"); - findLogEntry("testscript.js"); - findLogEntry("test-image.png"); - findLogEntry("network console"); - - finishTest(); + browser.removeEventListener(aEvent.type, onLoad, true); + openConsole(null, function() { + browser.addEventListener("load", testBasicNetLogging, true); + content.location = TEST_NETWORK_URI; + }); +} + +function testBasicNetLogging(aEvent) { + browser.removeEventListener(aEvent.type, testBasicNetLogging, true); + + outputNode = HUDService.getHudByWindow(content).outputNode; + + waitForSuccess({ + name: "network console message", + validatorFn: function() + { + return outputNode.textContent.indexOf("running network console") > -1; + }, + successFn: function() + { + findLogEntry("test-network.html"); + findLogEntry("testscript.js"); + findLogEntry("test-image.png"); + finishTest(); + }, + failureFn: finishTest, }); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js b/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js index 9d94721b55b..33fdc6e9e55 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js @@ -18,32 +18,61 @@ function test() { function testGroups(HUD) { let jsterm = HUD.jsterm; let outputNode = HUD.outputNode; + jsterm.clearOutput(); // We test for one group by testing for zero "new" groups. The // "webconsole-new-group" class creates a divider. Thus one group is // indicated by zero new groups, two groups are indicated by one new group, // and so on. + let waitForSecondMessage = { + name: "second console message", + validatorFn: function() + { + return outputNode.querySelectorAll(".webconsole-msg-output").length == 2; + }, + successFn: function() + { + let timestamp1 = Date.now(); + if (timestamp1 - timestamp0 < 5000) { + is(outputNode.querySelectorAll(".webconsole-new-group").length, 0, + "no group dividers exist after the second console message"); + } + + for (let i = 0; i < outputNode.itemCount; i++) { + outputNode.getItemAtIndex(i).timestamp = 0; // a "far past" value + } + + jsterm.execute("2"); + waitForSuccess(waitForThirdMessage); + }, + failureFn: finishTest, + }; + + let waitForThirdMessage = { + name: "one group divider exists after the third console message", + validatorFn: function() + { + return outputNode.querySelectorAll(".webconsole-new-group").length == 1; + }, + successFn: finishTest, + failureFn: finishTest, + }; + let timestamp0 = Date.now(); jsterm.execute("0"); - is(outputNode.querySelectorAll(".webconsole-new-group").length, 0, - "no group dividers exist after the first console message"); - jsterm.execute("1"); - let timestamp1 = Date.now(); - if (timestamp1 - timestamp0 < 5000) { - is(outputNode.querySelectorAll(".webconsole-new-group").length, 0, - "no group dividers exist after the second console message"); - } - - for (let i = 0; i < outputNode.itemCount; i++) { - outputNode.getItemAtIndex(i).timestamp = 0; // a "far past" value - } - - jsterm.execute("2"); - is(outputNode.querySelectorAll(".webconsole-new-group").length, 1, - "one group divider exists after the third console message"); - - finishTest(); + waitForSuccess({ + name: "no group dividers exist after the first console message", + validatorFn: function() + { + return outputNode.querySelectorAll(".webconsole-new-group").length == 0; + }, + successFn: function() + { + jsterm.execute("1"); + waitForSuccess(waitForSecondMessage); + }, + failureFn: finishTest, + }); } - diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js b/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js index b2ff1432b31..1f142ce741d 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js @@ -96,21 +96,6 @@ function testGen() { is(countMessageNodes(), 30, "there are 30 message nodes in the output " + "when the log limit is set to 30"); - prefBranch.setIntPref("console", 0); - console.log("baz"); - - waitForSuccess({ - name: "clear output", - validatorFn: function() - { - return countMessageNodes() == 0; - }, - successFn: testNext, - failureFn: finishTest, - }); - - yield; - prefBranch.clearUserPref("console"); hud = testDriver = prefBranch = console = outputNode = null; finishTest(); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js index 55bddd93df4..504bdaf5589 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597460_filter_scroll.js @@ -23,18 +23,36 @@ function consoleOpened(aHud) { hud.filterBox.value = "test message"; HUDService.updateFilterText(hud.filterBox); - browser.addEventListener("load", tabReload, true); + let waitForNetwork = { + name: "network message", + validatorFn: function() + { + return hud.outputNode.querySelector(".webconsole-msg-network"); + }, + successFn: testScroll, + failureFn: finishTest, + }; - executeSoon(function() { - content.location.reload(); + waitForSuccess({ + name: "console messages displayed", + validatorFn: function() + { + return hud.outputNode.textContent.indexOf("test message 199") > -1; + }, + successFn: function() + { + browser.addEventListener("load", function onReload() { + browser.removeEventListener("load", onReload, true); + waitForSuccess(waitForNetwork); + }, true); + content.location.reload(); + }, + failureFn: finishTest, }); } -function tabReload(aEvent) { - browser.removeEventListener(aEvent.type, tabReload, true); - +function testScroll() { let msgNode = hud.outputNode.querySelector(".webconsole-msg-network"); - ok(msgNode, "found network message"); ok(msgNode.classList.contains("hud-filtered-by-type"), "network message is filtered by type"); ok(msgNode.classList.contains("hud-filtered-by-string"), diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js b/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js index a76295b5092..978c1588031 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613013_console_api_iframe.js @@ -22,24 +22,28 @@ let TestObserver = { }; function tabLoad(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); + browser.removeEventListener(aEvent.type, tabLoad, true); - openConsole(); - - let hudId = HUDService.getHudIdByWindow(content); - hud = HUDService.hudReferences[hudId]; - - Services.obs.addObserver(TestObserver, "console-api-log-event", false); - content.location.reload(); + openConsole(null, function(aHud) { + hud = aHud; + Services.obs.addObserver(TestObserver, "console-api-log-event", false); + content.location.reload(); + }); } function performTest() { - isnot(hud.outputNode.textContent.indexOf("foobarBug613013"), -1, - "console.log() message found"); - Services.obs.removeObserver(TestObserver, "console-api-log-event"); TestObserver = null; - finishTest(); + + waitForSuccess({ + name: "console.log() message", + validatorFn: function() + { + return hud.outputNode.textContent.indexOf("foobarBug613013") > -1; + }, + successFn: finishTest, + failureFn: finishTest, + }); } function test() { diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js b/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js index dd209380665..279fc2967a0 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js @@ -17,6 +17,8 @@ function consoleOpened(hud) { content.console.log("test message " + i); } + let oldScrollTop = -1; + waitForSuccess({ name: "console.log messages displayed", validatorFn: function() @@ -25,11 +27,24 @@ function consoleOpened(hud) { }, successFn: function() { - let oldScrollTop = boxObject.scrollTop; + oldScrollTop = boxObject.scrollTop; ok(oldScrollTop > 0, "scroll location is not at the top"); hud.jsterm.execute("'hello world'"); + waitForSuccess(waitForExecute); + }, + failureFn: finishTest, + }); + + let waitForExecute = { + name: "jsterm output displayed", + validatorFn: function() + { + return outputNode.querySelector(".webconsole-msg-output"); + }, + successFn: function() + { isnot(boxObject.scrollTop, oldScrollTop, "scroll location updated"); oldScrollTop = boxObject.scrollTop; @@ -40,7 +55,7 @@ function consoleOpened(hud) { finishTest(); }, failureFn: finishTest, - }); + }; } function test() { diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js b/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js index 017b3f90151..cf3d2e3010e 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js @@ -56,14 +56,15 @@ function testWebDevLimits2() { } waitForSuccess({ - name: "11 console.log messages displayed", + name: "10 console.log messages displayed and one pruned", validatorFn: function() { - return outputNode.textContent.indexOf("test message 10") > -1; + let message0 = outputNode.textContent.indexOf("test message 0"); + let message10 = outputNode.textContent.indexOf("test message 10"); + return message0 == -1 && message10 > -1; }, successFn: function() { - testLogEntry(outputNode, "test message 0", "first message is pruned", false, true); findLogEntry("test message 1"); // Check if the sentinel entry is still there. findLogEntry("bar is not defined"); @@ -161,14 +162,28 @@ function loadImage() { gCounter++; return; } - is(gCounter, 11, "loaded 11 files"); - testLogEntry(outputNode, "test-image.png?_fubar=0", "first message is pruned", false, true); - findLogEntry("test-image.png?_fubar=1"); - // Check if the sentinel entry is still there. - findLogEntry("testing Net limits"); - Services.prefs.setIntPref("devtools.hud.loglimit.network", gOldPref); - testCssLimits(); + is(gCounter, 11, "loaded 11 files"); + + waitForSuccess({ + name: "loaded 11 files, one message pruned", + validatorFn: function() + { + let message0 = outputNode.querySelector('*[value*="test-image.png?_fubar=0"]'); + let message10 = outputNode.querySelector('*[value*="test-image.png?_fubar=10"]'); + return !message0 && message10; + }, + successFn: function() + { + findLogEntry("test-image.png?_fubar=1"); + // Check if the sentinel entry is still there. + findLogEntry("testing Net limits"); + + Services.prefs.setIntPref("devtools.hud.loglimit.network", gOldPref); + testCssLimits(); + }, + failureFn: testCssLimits, + }); } function testCssLimits() { diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js b/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js index 432e55cbecc..89841ce8454 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_646025_console_file_location.js @@ -16,29 +16,38 @@ function test() { } function onLoad(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - openConsole(); - hudId = HUDService.getHudIdByWindow(content); - - browser.addEventListener("load", testConsoleFileLocation, true); - content.location = TEST_URI; -} - -function testConsoleFileLocation(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - - outputNode = HUDService.hudReferences[hudId].outputNode; - - executeSoon(function() { - findLogEntry("test-file-location.js"); - findLogEntry("message for level"); - findLogEntry("test-file-location.js:5"); - findLogEntry("test-file-location.js:6"); - findLogEntry("test-file-location.js:7"); - findLogEntry("test-file-location.js:8"); - findLogEntry("test-file-location.js:9"); - - finishTest(); + browser.removeEventListener(aEvent.type, onLoad, true); + openConsole(null, function(aHud) { + hud = aHud; + browser.addEventListener("load", testConsoleFileLocation, true); + content.location = TEST_URI; + }); +} + +function testConsoleFileLocation(aEvent) { + browser.removeEventListener(aEvent.type, testConsoleFileLocation, true); + + outputNode = hud.outputNode; + + waitForSuccess({ + name: "console API messages", + validatorFn: function() + { + return outputNode.textContent.indexOf("message for level debug") > -1; + }, + successFn: function() + { + findLogEntry("test-file-location.js"); + findLogEntry("message for level"); + findLogEntry("test-file-location.js:5"); + findLogEntry("test-file-location.js:6"); + findLogEntry("test-file-location.js:7"); + findLogEntry("test-file-location.js:8"); + findLogEntry("test-file-location.js:9"); + + finishTest(); + }, + failureFn: finishTest, }); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js b/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js index 7dd6e766340..fac1f38960b 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js @@ -18,18 +18,25 @@ function test() { function consoleOpened(hud) { outputNode = hud.outputNode; - executeSoon(function() { - findLogEntry("aTimer: timer started"); - findLogEntry("ms"); - - // The next test makes sure that timers with the same name but in separate - // tabs, do not contain the same value. - addTab("data:text/html;charset=utf-8,"); - browser.addEventListener("load", function onLoad() { - browser.removeEventListener("load", onLoad, true); - openConsole(null, testTimerIndependenceInTabs); - }, true); + waitForSuccess({ + name: "aTimer started", + validatorFn: function() + { + return outputNode.textContent.indexOf("aTimer: timer started") > -1; + }, + successFn: function() + { + findLogEntry("ms"); + // The next test makes sure that timers with the same name but in separate + // tabs, do not contain the same value. + addTab("data:text/html;charset=utf-8,"); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, testTimerIndependenceInTabs); + }, true); + }, + failureFn: finishTest, }); } @@ -56,22 +63,30 @@ function testTimerIndependenceInSameTab() { let hud = HUDService.hudReferences[hudId]; outputNode = hud.outputNode; - executeSoon(function() { - findLogEntry("bTimer: timer started"); - hud.jsterm.clearOutput(); + waitForSuccess({ + name: "bTimer started", + validatorFn: function() + { + return outputNode.textContent.indexOf("bTimer: timer started") > -1; + }, + successFn: function() { + hud.jsterm.clearOutput(); - // Now the following console.timeEnd() call shouldn't display anything, - // if the timers in different pages are not related. - browser.addEventListener("load", function onLoad() { - browser.removeEventListener("load", onLoad, true); - executeSoon(testTimerIndependenceInSameTabAgain); - }, true); - content.location = "data:text/html;charset=utf-8,"; + // Now the following console.timeEnd() call shouldn't display anything, + // if the timers in different pages are not related. + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + executeSoon(testTimerIndependenceInSameTabAgain); + }, true); + content.location = "data:text/html;charset=utf-8," + + ""; + }, + failureFn: finishTest, }); } -function testTimerIndependenceInSameTabAgain(hud) { +function testTimerIndependenceInSameTabAgain() { let hudId = HUDService.getHudIdByWindow(content); let hud = HUDService.hudReferences[hudId]; outputNode = hud.outputNode; diff --git a/dom/base/ConsoleAPI.js b/dom/base/ConsoleAPI.js index 5579d660dbe..867229d339e 100644 --- a/dom/base/ConsoleAPI.js +++ b/dom/base/ConsoleAPI.js @@ -7,13 +7,28 @@ let Cu = Components.utils; let Ci = Components.interfaces; let Cc = Components.classes; + // The maximum allowed number of concurrent timers per page. const MAX_PAGE_TIMERS = 10000; +// The regular expression used to parse %s/%d and other placeholders for +// variables in strings that need to be interpolated. +const ARGUMENT_PATTERN = /%\d*\.?\d*([osdif])\b/g; + +// The maximum stacktrace depth when populating the stacktrace array used for +// console.trace(). +const DEFAULT_MAX_STACKTRACE_DEPTH = 200; + +// The console API methods are async and their action is executed later. This +// delay tells how much later. +const CALL_DELAY = 30; // milliseconds + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm"); +let nsITimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + function ConsoleAPI() {} ConsoleAPI.prototype = { @@ -21,11 +36,17 @@ ConsoleAPI.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer]), + _timerInitialized: false, + _queuedCalls: null, + _timerCallback: null, + _destroyedWindows: null, + // nsIDOMGlobalPropertyInitializer init: function CA_init(aWindow) { Services.obs.addObserver(this, "xpcom-shutdown", false); Services.obs.addObserver(this, "inner-window-destroyed", false); + let outerID; let innerID; try { @@ -39,45 +60,50 @@ ConsoleAPI.prototype = { Cu.reportError(ex); } + let meta = { + outerID: outerID, + innerID: innerID, + }; + let self = this; let chromeObject = { // window.console API log: function CA_log() { - self.notifyObservers(outerID, innerID, "log", self.processArguments(arguments)); + self.queueCall("log", arguments, meta); }, info: function CA_info() { - self.notifyObservers(outerID, innerID, "info", self.processArguments(arguments)); + self.queueCall("info", arguments, meta); }, warn: function CA_warn() { - self.notifyObservers(outerID, innerID, "warn", self.processArguments(arguments)); + self.queueCall("warn", arguments, meta); }, error: function CA_error() { - self.notifyObservers(outerID, innerID, "error", self.processArguments(arguments)); + self.queueCall("error", arguments, meta); }, debug: function CA_debug() { - self.notifyObservers(outerID, innerID, "log", self.processArguments(arguments)); + self.queueCall("debug", arguments, meta); }, trace: function CA_trace() { - self.notifyObservers(outerID, innerID, "trace", self.getStackTrace()); + self.queueCall("trace", arguments, meta); }, // Displays an interactive listing of all the properties of an object. dir: function CA_dir() { - self.notifyObservers(outerID, innerID, "dir", arguments); + self.queueCall("dir", arguments, meta); }, group: function CA_group() { - self.notifyObservers(outerID, innerID, "group", self.beginGroup(arguments)); + self.queueCall("group", arguments, meta); }, groupCollapsed: function CA_groupCollapsed() { - self.notifyObservers(outerID, innerID, "groupCollapsed", self.beginGroup(arguments)); + self.queueCall("groupCollapsed", arguments, meta); }, groupEnd: function CA_groupEnd() { - self.notifyObservers(outerID, innerID, "groupEnd", arguments); + self.queueCall("groupEnd", arguments, meta); }, time: function CA_time() { - self.notifyObservers(outerID, innerID, "time", self.startTimer(innerID, arguments[0])); + self.queueCall("time", arguments, meta); }, timeEnd: function CA_timeEnd() { - self.notifyObservers(outerID, innerID, "timeEnd", self.stopTimer(innerID, arguments[0])); + self.queueCall("timeEnd", arguments, meta); }, __exposedProps__: { log: "r", @@ -123,6 +149,12 @@ ConsoleAPI.prototype = { Object.defineProperties(contentObj, properties); Cu.makeObjectPropsNormal(contentObj); + this._queuedCalls = []; + this._destroyedWindows = []; + this._timerCallback = { + notify: this._timerCallbackNotify.bind(this), + }; + return contentObj; }, @@ -131,51 +163,144 @@ ConsoleAPI.prototype = { if (aTopic == "xpcom-shutdown") { Services.obs.removeObserver(this, "xpcom-shutdown"); Services.obs.removeObserver(this, "inner-window-destroyed"); + this._destroyedWindows = []; + this._queuedCalls = []; } else if (aTopic == "inner-window-destroyed") { let innerWindowID = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; delete this.timerRegistry[innerWindowID + ""]; + this._destroyedWindows.push(innerWindowID); } }, + /** + * Queue a call to a console method. See the CALL_DELAY constant. + * + * @param string aMethod + * The console method the code has invoked. + * @param object aArguments + * The arguments passed to the console method. + * @param object aMeta + * The associated call meta information. This needs to hold the inner + * and outer window IDs from where the console method was called. + */ + queueCall: function CA_queueCall(aMethod, aArguments, aMeta) + { + let metaForCall = { + outerID: aMeta.outerID, + innerID: aMeta.innerID, + timeStamp: Date.now(), + stack: this.getStackTrace(aMethod != "trace" ? 1 : null), + }; + + this._queuedCalls.push([aMethod, aArguments, metaForCall]); + + if (!this._timerInitialized) { + nsITimer.initWithCallback(this._timerCallback, CALL_DELAY, + Ci.nsITimer.TYPE_ONE_SHOT); + this._timerInitialized = true; + } + }, + + /** + * Timer callback used to process each of the queued calls. + * @private + */ + _timerCallbackNotify: function CA__timerCallbackNotify() + { + this._timerInitialized = false; + this._queuedCalls.splice(0).forEach(this._processQueuedCall, this); + this._destroyedWindows = []; + }, + + /** + * Process a queued call to a console method. + * + * @private + * @param array aCall + * Array that holds information about the queued call. + */ + _processQueuedCall: function CA__processQueuedItem(aCall) + { + let [method, args, meta] = aCall; + + let notifyMeta = { + outerID: meta.outerID, + innerID: meta.innerID, + timeStamp: meta.timeStamp, + frame: meta.stack[0], + }; + + let notifyArguments = null; + + switch (method) { + case "log": + case "info": + case "warn": + case "error": + case "debug": + notifyArguments = this.processArguments(args); + break; + case "trace": + notifyArguments = meta.stack; + break; + case "group": + case "groupCollapsed": + notifyArguments = this.beginGroup(args); + break; + case "groupEnd": + case "dir": + notifyArguments = args; + break; + case "time": + notifyArguments = this.startTimer(meta.innerID, args[0], meta.timeStamp); + break; + case "timeEnd": + notifyArguments = this.stopTimer(meta.innerID, args[0], meta.timeStamp); + break; + default: + // unknown console API method! + return; + } + + this.notifyObservers(method, notifyArguments, notifyMeta); + }, + /** * Notify all observers of any console API call. * - * @param number aOuterWindowID - * The outer window ID from where the message came from. - * @param number aInnerWindowID - * The inner window ID from where the message came from. * @param string aLevel * The message level. * @param mixed aArguments * The arguments given to the console API call. - **/ - notifyObservers: - function CA_notifyObservers(aOuterWindowID, aInnerWindowID, aLevel, aArguments) { - if (!aOuterWindowID) { - return; - } - - let stack = this.getStackTrace(); - // Skip the first frame since it contains an internal call. - let frame = stack[1]; + * @param object aMeta + * Object that holds metadata about the console API call: + * - outerID - the outer ID of the window where the message came from. + * - innerID - the inner ID of the window where the message came from. + * - frame - the youngest content frame in the call stack. + * - timeStamp - when the console API call occurred. + */ + notifyObservers: function CA_notifyObservers(aLevel, aArguments, aMeta) { let consoleEvent = { - ID: aOuterWindowID, - innerID: aInnerWindowID, + ID: aMeta.outerID, + innerID: aMeta.innerID, level: aLevel, - filename: frame.filename, - lineNumber: frame.lineNumber, - functionName: frame.functionName, + filename: aMeta.frame.filename, + lineNumber: aMeta.frame.lineNumber, + functionName: aMeta.frame.functionName, arguments: aArguments, - timeStamp: Date.now(), + timeStamp: aMeta.timeStamp, }; consoleEvent.wrappedJSObject = consoleEvent; - ConsoleAPIStorage.recordEvent(aInnerWindowID, consoleEvent); + // Store messages for which the inner window was not destroyed. + if (this._destroyedWindows.indexOf(aMeta.innerID) == -1) { + ConsoleAPIStorage.recordEvent(aMeta.innerID, consoleEvent); + } - Services.obs.notifyObservers(consoleEvent, - "console-api-log-event", aOuterWindowID); + Services.obs.notifyObservers(consoleEvent, "console-api-log-event", + aMeta.outerID); }, /** @@ -189,18 +314,14 @@ ConsoleAPI.prototype = { * The arguments given to the console API call. **/ processArguments: function CA_processArguments(aArguments) { - if (aArguments.length < 2) { + if (aArguments.length < 2 || typeof aArguments[0] != "string") { return aArguments; } let args = Array.prototype.slice.call(aArguments); let format = args.shift(); - if (typeof format != "string") { - return aArguments; - } // Format specification regular expression. - let pattern = /%(\d*).?(\d*)[a-zA-Z]/g; - let processed = format.replace(pattern, function CA_PA_substitute(spec) { - switch (spec[spec.length-1]) { + let processed = format.replace(ARGUMENT_PATTERN, function CA_PA_substitute(match, submatch) { + switch (submatch) { case "o": case "s": return String(args.shift()); @@ -210,7 +331,7 @@ ConsoleAPI.prototype = { case "f": return parseFloat(args.shift()); default: - return spec; + return submatch; }; }); args.unshift(processed); @@ -220,13 +341,19 @@ ConsoleAPI.prototype = { /** * Build the stacktrace array for the console.trace() call. * + * @param number [aMaxDepth=DEFAULT_MAX_STACKTRACE_DEPTH] + * Optional maximum stacktrace depth. * @return array * Each element is a stack frame that holds the following properties: * filename, lineNumber, functionName and language. - **/ - getStackTrace: function CA_getStackTrace() { + */ + getStackTrace: function CA_getStackTrace(aMaxDepth) { + if (!aMaxDepth) { + aMaxDepth = DEFAULT_MAX_STACKTRACE_DEPTH; + } + let stack = []; - let frame = Components.stack.caller; + let frame = Components.stack.caller.caller; while (frame = frame.caller) { if (frame.language == Ci.nsIProgrammingLanguage.JAVASCRIPT || frame.language == Ci.nsIProgrammingLanguage.JAVASCRIPT2) { @@ -236,6 +363,9 @@ ConsoleAPI.prototype = { functionName: frame.name, language: frame.language, }); + if (stack.length == aMaxDepth) { + break; + } } } @@ -265,13 +395,15 @@ ConsoleAPI.prototype = { * The inner ID of the window. * @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. **/ - startTimer: function CA_startTimer(aWindowId, aName) { + startTimer: function CA_startTimer(aWindowId, aName, aTimestamp) { if (!aName) { return; } @@ -285,7 +417,7 @@ ConsoleAPI.prototype = { } let key = aWindowId + "-" + aName.toString(); if (!pageTimers[key]) { - pageTimers[key] = Date.now(); + pageTimers[key] = aTimestamp || Date.now(); } return { name: aName, started: pageTimers[key] }; }, @@ -297,11 +429,13 @@ ConsoleAPI.prototype = { * The inner ID of the window. * @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. **/ - stopTimer: function CA_stopTimer(aWindowId, aName) { + stopTimer: function CA_stopTimer(aWindowId, aName, aTimestamp) { if (!aName) { return; } @@ -314,7 +448,7 @@ ConsoleAPI.prototype = { if (!pageTimers[key]) { return; } - let duration = Date.now() - pageTimers[key]; + let duration = (aTimestamp || Date.now()) - pageTimers[key]; delete pageTimers[key]; return { name: aName, duration: duration }; } diff --git a/dom/tests/browser/browser_ConsoleAPITests.js b/dom/tests/browser/browser_ConsoleAPITests.js index e24b708d146..bfd32aa5c3b 100644 --- a/dom/tests/browser/browser_ConsoleAPITests.js +++ b/dom/tests/browser/browser_ConsoleAPITests.js @@ -5,7 +5,7 @@ const TEST_URI = "http://example.com/browser/dom/tests/browser/test-console-api.html"; -var gWindow, gLevel, gArgs; +var gWindow, gLevel, gArgs, gTestDriver; function test() { waitForExplicitFinish(); @@ -15,6 +15,7 @@ function test() { var browser = gBrowser.selectedBrowser; registerCleanupFunction(function () { + gWindow = gLevel = gArgs = gTestDriver = null; gBrowser.removeTab(tab); }); @@ -25,7 +26,8 @@ function test() { executeSoon(function test_executeSoon() { gWindow = browser.contentWindow; consoleAPISanityTest(); - observeConsoleTest(); + gTestDriver = observeConsoleTest(); + gTestDriver.next(); }); }, false); @@ -42,9 +44,6 @@ function testConsoleData(aMessageObject) { if (gLevel == "trace") { is(aMessageObject.arguments.toSource(), gArgs.toSource(), "stack trace is correct"); - - // Now test the location information in console.log() - startLocationTest(); } else { gArgs.forEach(function (a, i) { @@ -52,10 +51,7 @@ function testConsoleData(aMessageObject) { }); } - if (aMessageObject.level == "error") { - // Now test console.trace() - startTraceTest(); - } + gTestDriver.next(); } function testLocationData(aMessageObject) { @@ -163,9 +159,11 @@ function observeConsoleTest() { let win = XPCNativeWrapper.unwrap(gWindow); expect("log", "arg"); win.console.log("arg"); + yield; expect("info", "arg", "extra arg"); win.console.info("arg", "extra arg"); + yield; // We don't currently support width and precision qualifiers, but we don't // choke on them either. @@ -174,29 +172,49 @@ function observeConsoleTest() { 1, "PI", 3.14159); + yield; + expect("log", "%d, %s, %l"); win.console.log("%d, %s, %l"); + yield; + expect("log", "%a %b %c"); win.console.log("%a %b %c"); + yield; + expect("log", "%a %b %c", "a", "b"); win.console.log("%a %b %c", "a", "b"); + yield; + expect("log", "2, a, %l", 3); win.console.log("%d, %s, %l", 2, "a", 3); + yield; // Bug #692550 handle null and undefined. expect("log", "null, undefined"); win.console.log("%s, %s", null, undefined); + yield; // Bug #696288 handle object as first argument. let obj = { a: 1 }; expect("log", obj, "a"); win.console.log(obj, "a"); + yield; expect("dir", win.toString()); win.console.dir(win); + yield; expect("error", "arg"); win.console.error("arg"); + + yield; + + startTraceTest(); + yield; + + startLocationTest(); + yield; } function consoleAPISanityTest() { diff --git a/dom/tests/browser/browser_ConsoleStoragePBTest.js b/dom/tests/browser/browser_ConsoleStoragePBTest.js index 2a44a09d501..4da2b8940dc 100644 --- a/dom/tests/browser/browser_ConsoleStoragePBTest.js +++ b/dom/tests/browser/browser_ConsoleStoragePBTest.js @@ -14,24 +14,44 @@ function test() { var CSS = {}; Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", CSS); - function checkStorageOccurs(shouldOccur) { + let innerID, beforeEvents, storageShouldOccur; + + var ConsoleObserver = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + observe: function CO_observe(aSubject, aTopic, aData) + { + if (aTopic != "console-api-log-event") { + return; + } + + let afterEvents = CSS.ConsoleAPIStorage.getEvents(innerID); + + is(beforeEvents.length == afterEvents.length - 1, + storageShouldOccur, + "storage should" + (storageShouldOccur ? "" : " not") + " occur"); + + executeSoon(function() { + Services.obs.removeObserver(ConsoleObserver, "console-api-log-event"); + pb.privateBrowsingEnabled = storageShouldOccur; + }); + } + }; + + function checkStorageOccurs() { + Services.obs.addObserver(ConsoleObserver, "console-api-log-event", false); + let win = XPCNativeWrapper.unwrap(browser.contentWindow); - let innerID = getInnerWindowId(win); + innerID = getInnerWindowId(win); - let beforeEvents = CSS.ConsoleAPIStorage.getEvents(innerID); - win.console.log("foo bar baz (private: " + !shouldOccur + ")"); - - let afterEvents = CSS.ConsoleAPIStorage.getEvents(innerID); - - is(beforeEvents.length == afterEvents.length - 1, - shouldOccur, "storage should" + (shouldOccur ? "" : "n't") + " occur"); + beforeEvents = CSS.ConsoleAPIStorage.getEvents(innerID); + win.console.log("foo bar baz (private: " + !storageShouldOccur + ")"); } function pbObserver(aSubject, aTopic, aData) { if (aData == "enter") { - checkStorageOccurs(false); - - executeSoon(function () { pb.privateBrowsingEnabled = false; }); + storageShouldOccur = false; + checkStorageOccurs(); } else if (aData == "exit") { executeSoon(finish); } @@ -61,9 +81,8 @@ function test() { browser.removeEventListener("DOMContentLoaded", onLoad, false); - checkStorageOccurs(true); - - pb.privateBrowsingEnabled = true; + storageShouldOccur = true; + checkStorageOccurs(); }, false); }