From f94af751abdf083ce8cd8908240c2e371d018326 Mon Sep 17 00:00:00 2001 From: Mihai Sucan Date: Fri, 2 Aug 2013 20:19:23 +0300 Subject: [PATCH] Bug 793996 - Create reload marker in the Web Console; r=robcee --- browser/devtools/webconsole/console-output.js | 311 ++++++++++++++++++ browser/devtools/webconsole/test/Makefile.in | 2 +- .../test/browser_console_navigation_marker.js | 81 +++++ .../browser_webconsole_bug_580400_groups.js | 78 ----- .../test/browser_webconsole_bug_632817.js | 20 +- browser/devtools/webconsole/test/head.js | 25 +- browser/devtools/webconsole/webconsole.js | 100 +++--- .../themes/shared/devtools/webconsole.inc.css | 14 + 8 files changed, 489 insertions(+), 142 deletions(-) create mode 100644 browser/devtools/webconsole/console-output.js create mode 100644 browser/devtools/webconsole/test/browser_console_navigation_marker.js delete mode 100644 browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js diff --git a/browser/devtools/webconsole/console-output.js b/browser/devtools/webconsole/console-output.js new file mode 100644 index 000000000000..4a11c88f1425 --- /dev/null +++ b/browser/devtools/webconsole/console-output.js @@ -0,0 +1,311 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Heritage = require("sdk/core/heritage"); +const XHTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +// Constants for compatibility with the Web Console output implementation before +// bug 778766. +// TODO: remove these once bug 778766 is fixed. +const COMPAT = { + // The various categories of messages. + CATEGORIES: { + NETWORK: 0, + CSS: 1, + JS: 2, + WEBDEV: 3, + INPUT: 4, + OUTPUT: 5, + SECURITY: 6, + }, + + // The possible message severities. + SEVERITIES: { + ERROR: 0, + WARNING: 1, + INFO: 2, + LOG: 3, + }, +}; + +/** + * The ConsoleOutput object is used to manage output of messages in the Web + * Console. + * + * @constructor + * @param object owner + * The console output owner. This usually the WebConsoleFrame instance. + * Any other object can be used, as long as it has the following + * properties and methods: + * - window + * - document + * - outputMessage(category, methodOrNode[, methodArguments]) + * TODO: this is needed temporarily, until bug 778766 is fixed. + */ +function ConsoleOutput(owner) +{ + this.owner = owner; + this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this); +} + +ConsoleOutput.prototype = { + /** + * The document that holds the output. + * @type DOMDocument + */ + get document() this.owner.document, + + /** + * The DOM window that holds the output. + * @type Window + */ + get window() this.owner.window, + + /** + * Add a message to output. + * + * @param object ...args + * Any number of Message objects. + * @return this + */ + addMessage: function(...args) + { + for (let msg of args) { + msg.init(this); + this.owner.outputMessage(msg._categoryCompat, this._onFlushOutputMessage, + [msg]); + } + return this; + }, + + /** + * Message renderer used for compatibility with the current Web Console output + * implementation. This method is invoked for every message object that is + * flushed to output. The message object is initialized and rendered, then it + * is displayed. + * + * TODO: remove this method once bug 778766 is fixed. + * + * @private + * @param object message + * The message object to render. + * @return DOMElement + * The message DOM element that can be added to the console output. + */ + _onFlushOutputMessage: function(message) + { + return message.render().element; + }, + + /** + * Destroy this ConsoleOutput instance. + */ + destroy: function() + { + this.owner = null; + }, +}; // ConsoleOutput.prototype + +/** + * Message objects container. + * @type object + */ +let Messages = {}; + +/** + * The BaseMessage object is used for all types of messages. Every kind of + * message should use this object as its base. + * + * @constructor + */ +Messages.BaseMessage = function() +{ + this.widgets = new Set(); +}; + +Messages.BaseMessage.prototype = { + /** + * Reference to the ConsoleOutput owner. + * + * @type object|null + * This is |null| if the message is not yet initialized. + */ + output: null, + + /** + * Reference to the parent message object, if this message is in a group or if + * it is otherwise owned by another message. + * + * @type object|null + */ + parent: null, + + /** + * Message DOM element. + * + * @type DOMElement|null + * This is |null| if the message is not yet rendered. + */ + element: null, + + /** + * Tells if this message is visible or not. + * @type boolean + */ + get visible() { + return this.element && this.element.parentNode; + }, + + /** + * Holds the text-only representation of the message. + * @type string + */ + textContent: "", + + /** + * Set of widgets included in this message. + * @type Set + */ + widgets: null, + + // Properties that allow compatibility with the current Web Console output + // implementation. + _elementClassCompat: "", + _categoryCompat: null, + _severityCompat: null, + + /** + * Initialize the message. + * + * @param object output + * The ConsoleOutput owner. + * @param object [parent=null] + * Optional: a different message object that owns this instance. + * @return this + */ + init: function(output, parent=null) + { + this.output = output; + this.parent = parent; + return this; + }, + + /** + * Render the message. After this method is invoked the |element| property + * will point to the DOM element of this message. + * @return this + */ + render: function() + { + if (!this.element) { + this.element = this._renderCompat(); + } + return this; + }, + + /** + * Prepare the message container for the Web Console, such that it is + * compatible with the current implementation. + * TODO: remove this once bug 778766. + */ + _renderCompat: function() + { + let doc = this.output.document; + let container = doc.createElementNS(XUL_NS, "richlistitem"); + container.setAttribute("id", "console-msg-" + gSequenceId()); + container.setAttribute("class", "hud-msg-node " + this._elementClassCompat); + container.category = this._categoryCompat; + container.severity = this._severityCompat; + container.clipboardText = this.textContent; + container.timestamp = this.timestamp; + container._messageObject = this; + + let body = doc.createElementNS(XUL_NS, "description"); + body.flex = 1; + body.classList.add("webconsole-msg-body"); + container.appendChild(body); + + return container; + }, +}; // Messages.BaseMessage.prototype + + +/** + * The NavigationMarker is used to show a page load event. + * + * @constructor + * @extends Messages.BaseMessage + * @param string url + * The URL to display. + * @param number timestamp + * The message date and time, milliseconds elapsed since 1 January 1970 + * 00:00:00 UTC. + */ +Messages.NavigationMarker = function(url, timestamp) +{ + Messages.BaseMessage.apply(this, arguments); + this._url = url; + this.textContent = "------ " + url; + this.timestamp = timestamp; +}; + +Messages.NavigationMarker.prototype = Heritage.extend(Messages.BaseMessage.prototype, +{ + /** + * Message timestamp. + * + * @type number + * Milliseconds elapsed since 1 January 1970 00:00:00 UTC. + */ + timestamp: 0, + + // Class names in order: category, severity then the class for the filter. + _elementClassCompat: "webconsole-msg-network webconsole-msg-info hud-networkinfo", + _categoryCompat: COMPAT.CATEGORIES.NETWORK, + _severityCompat: COMPAT.SEVERITIES.LOG, + + /** + * Prepare the DOM element for this message. + * @return this + */ + render: function() + { + if (this.element) { + return this; + } + + let url = this._url; + let pos = url.indexOf("?"); + if (pos > -1) { + url = url.substr(0, pos); + } + + let doc = this.output.document; + let urlnode = doc.createElementNS(XHTML_NS, "span"); + urlnode.className = "url"; + urlnode.textContent = url; + + // Add the text in the xul:description.webconsole-msg-body element. + let render = Messages.BaseMessage.prototype.render.bind(this); + render().element.firstChild.appendChild(urlnode); + this.element.classList.add("navigation-marker"); + this.element.url = this._url; + + return this; + }, +}); // Messages.NavigationMarker.prototype + + +function gSequenceId() +{ + return gSequenceId.n++; +} +gSequenceId.n = 0; + +exports.ConsoleOutput = ConsoleOutput; +exports.Messages = Messages; diff --git a/browser/devtools/webconsole/test/Makefile.in b/browser/devtools/webconsole/test/Makefile.in index ef3265a21d75..3ba6dded23da 100644 --- a/browser/devtools/webconsole/test/Makefile.in +++ b/browser/devtools/webconsole/test/Makefile.in @@ -17,7 +17,6 @@ MOCHITEST_BROWSER_FILES = \ browser_webconsole_basic_net_logging.js \ browser_webconsole_bug_579412_input_focus.js \ browser_webconsole_bug_580001_closing_after_completion.js \ - browser_webconsole_bug_580400_groups.js \ browser_webconsole_bug_588730_text_node_insertion.js \ browser_webconsole_bug_601667_filter_buttons.js \ browser_webconsole_bug_597136_external_script_errors.js \ @@ -145,6 +144,7 @@ MOCHITEST_BROWSER_FILES = \ browser_console_variables_view_while_debugging_and_inspecting.js \ browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js \ browser_webconsole_cached_autocomplete.js \ + browser_console_navigation_marker.js \ head.js \ $(NULL) diff --git a/browser/devtools/webconsole/test/browser_console_navigation_marker.js b/browser/devtools/webconsole/test/browser_console_navigation_marker.js new file mode 100644 index 000000000000..6ec9d3432719 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_navigation_marker.js @@ -0,0 +1,81 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that the navigation marker shows on page reload - bug 793996. + +function test() +{ + const PREF = "devtools.webconsole.persistlog"; + const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; + let hud = null; + let Messages = require("devtools/webconsole/console-output").Messages; + + Services.prefs.setBoolPref(PREF, true); + registerCleanupFunction(() => Services.prefs.clearUserPref(PREF)); + + addTab(TEST_URI); + + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, consoleOpened); + }, true); + + function consoleOpened(aHud) + { + hud = aHud; + ok(hud, "Web Console opened"); + + hud.jsterm.clearOutput(); + content.console.log("foobarz1"); + waitForMessages({ + webconsole: hud, + messages: [{ + text: "foobarz1", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }).then(onConsoleMessage); + } + + function onConsoleMessage() + { + browser.addEventListener("load", onReload, true); + content.location.reload(); + } + + function onReload() + { + browser.removeEventListener("load", onReload, true); + + content.console.log("foobarz2"); + + waitForMessages({ + webconsole: hud, + messages: [{ + name: "page reload", + text: "test-console.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + }, + { + text: "foobarz2", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }, + { + name: "navigation marker", + text: "test-console.html", + type: Messages.NavigationMarker, + }], + }).then(onConsoleMessageAfterReload); + } + + function onConsoleMessageAfterReload() + { + isnot(hud.outputNode.textContent.indexOf("foobarz1"), -1, + "foobarz1 is still in the output"); + 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 deleted file mode 100644 index 33fdc6e9e550..000000000000 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_580400_groups.js +++ /dev/null @@ -1,78 +0,0 @@ -/* vim:set ts=2 sw=2 sts=2 et: */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// Tests that console groups behave properly. - -const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html"; - -function test() { - addTab(TEST_URI); - browser.addEventListener("load", function onLoad() { - browser.removeEventListener("load", onLoad, true); - openConsole(null, testGroups); - }, true); -} - -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"); - - 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_632817.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js index 5c7e29cff1fe..2bfafd5373ce 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js @@ -124,12 +124,20 @@ function testFormSubmission() // There should be 3 network requests pointing to the HTML file. waitForMessages({ webconsole: hud, - messages: [{ - text: "test-network-request.html", - category: CATEGORY_NETWORK, - severity: SEVERITY_LOG, - count: 3, - }], + messages: [ + { + text: "test-network-request.html", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + count: 3, + }, + { + text: "test-data.json", + category: CATEGORY_NETWORK, + severity: SEVERITY_LOG, + count: 2, + }, + ], }).then(testLiveFilteringOnSearchStrings); }; diff --git a/browser/devtools/webconsole/test/head.js b/browser/devtools/webconsole/test/head.js index ff31e787a171..9518e125df5f 100644 --- a/browser/devtools/webconsole/test/head.js +++ b/browser/devtools/webconsole/test/head.js @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -let WebConsoleUtils, gDevTools, TargetFactory, console, promise; +let WebConsoleUtils, gDevTools, TargetFactory, console, promise, require; (() => { gDevTools = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools; @@ -14,6 +14,7 @@ let WebConsoleUtils, gDevTools, TargetFactory, console, promise; let utils = tools.require("devtools/toolkit/webconsole/utils"); TargetFactory = tools.TargetFactory; WebConsoleUtils = utils.Utils; + require = tools.require; })(); // promise._reportErrors = true; // please never leave me. @@ -884,6 +885,9 @@ function getMessageElementText(aElement) * message. * - longString: boolean, set to |true} to match long strings in the * message. + * - type: match messages that are instances of the given object. For + * example, you can point to Messages.NavigationMarker to match any + * such message. * - objects: boolean, set to |true| if you expect inspectable * objects in the message. * - source: object that can hold one property: url. This is used to @@ -1063,8 +1067,25 @@ function waitForMessages(aOptions) return false; } + if (aRule.type) { + // The rule tries to match the newer types of messages, based on their + // object constructor. + if (!aElement._messageObject || + !(aElement._messageObject instanceof aRule.type)) { + return false; + } + } + else if (aElement._messageObject) { + // If the message element holds a reference to its object, it means this + // is a newer message type. All of the older waitForMessages() rules do + // not expect this kind of messages. We return false here. + // TODO: we keep this behavior until bug 778766 is fixed. After that we + // will not require |type| to match newer types of messages. + return false; + } + let partialMatch = !!(aRule.consoleTrace || aRule.consoleTime || - aRule.consoleTimeEnd); + aRule.consoleTimeEnd || aRule.type); if (aRule.category && aElement.category != aRule.category) { if (partialMatch) { diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index 1a79f2d7fcf3..e7085c542830 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -22,6 +22,10 @@ loader.lazyGetter(this, "ToolSidebar", () => require("devtools/framework/sidebar").ToolSidebar); loader.lazyGetter(this, "NetworkPanel", () => require("devtools/webconsole/network-panel").NetworkPanel); +loader.lazyGetter(this, "ConsoleOutput", + () => require("devtools/webconsole/console-output").ConsoleOutput); +loader.lazyGetter(this, "Messages", + () => require("devtools/webconsole/console-output").Messages); loader.lazyImporter(this, "GripClient", "resource://gre/modules/devtools/dbg-client.jsm"); loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm"); loader.lazyImporter(this, "VariablesViewController", "resource:///modules/devtools/VariablesViewController.jsm"); @@ -187,6 +191,8 @@ function WebConsoleFrame(aWebConsoleOwner) this._networkRequests = {}; this.filterPrefs = {}; + this.output = new ConsoleOutput(this); + this._toggleFilter = this._toggleFilter.bind(this); this._flushMessageQueue = this._flushMessageQueue.bind(this); @@ -324,6 +330,12 @@ WebConsoleFrame.prototype = { */ outputNode: null, + /** + * The ConsoleOutput instance that manages all output. + * @type object + */ + output: null, + /** * The input element that allows the user to filter messages by string. * @type nsIDOMElement @@ -830,8 +842,6 @@ WebConsoleFrame.prototype = { node.classList.add("hud-filtered-by-type"); } } - - this.regroupOutput(); }, /** @@ -858,8 +868,6 @@ WebConsoleFrame.prototype = { node.classList.add("hud-filtered-by-string"); } } - - this.regroupOutput(); }, /** @@ -1750,6 +1758,35 @@ WebConsoleFrame.prototype = { } }, + /** + * Handler for the tabNavigated notification. + * + * @param string aEvent + * Event name. + * @param object aPacket + * Notification packet received from the server. + */ + handleTabNavigated: function WCF_handleTabNavigated(aEvent, aPacket) + { + if (aEvent == "will-navigate") { + if (this.persistLog) { + let marker = new Messages.NavigationMarker(aPacket.url, Date.now()); + this.output.addMessage(marker); + } + else { + this.jsterm.clearOutput(); + } + } + + if (aPacket.url) { + this.onLocationChange(aPacket.url, aPacket.title); + } + + if (aEvent == "navigate" && !aPacket.nativeConsoleAPI) { + this.logWarningAboutReplacedAPI(); + } + }, + /** * Output a message node. This filters a node appropriately, then sends it to * the output, regrouping and pruning output as necessary. @@ -1863,11 +1900,6 @@ WebConsoleFrame.prototype = { this._pruneCategoriesQueue = {}; } - // Regroup messages at the end of the queue. - if (!this._outputQueue.length) { - this.regroupOutput(); - } - let isInputOutput = lastVisibleNode && (lastVisibleNode.classList.contains("webconsole-msg-input") || lastVisibleNode.classList.contains("webconsole-msg-output")); @@ -2136,29 +2168,6 @@ WebConsoleFrame.prototype = { } }, - /** - * Splits the given console messages into groups based on their timestamps. - */ - regroupOutput: function WCF_regroupOutput() - { - // Go through the nodes and adjust the placement of "webconsole-new-group" - // classes. - let nodes = this.outputNode.querySelectorAll(".hud-msg-node" + - ":not(.hud-filtered-by-string):not(.hud-filtered-by-type)"); - let lastTimestamp; - for (let i = 0, n = nodes.length; i < n; i++) { - let thisTimestamp = nodes[i].timestamp; - if (lastTimestamp != null && - thisTimestamp >= lastTimestamp + NEW_GROUP_DELAY) { - nodes[i].classList.add("webconsole-new-group"); - } - else { - nodes[i].classList.remove("webconsole-new-group"); - } - lastTimestamp = thisTimestamp; - } - }, - /** * Given a category and message body, creates a DOM node to represent an * incoming message. The timestamp is automatically added. @@ -2622,7 +2631,6 @@ WebConsoleFrame.prototype = { // Gather up the selected items and concatenate their clipboard text. let strings = []; - let newGroup = false; let children = this.outputNode.children; @@ -2632,21 +2640,10 @@ WebConsoleFrame.prototype = { continue; } - // Add dashes between groups so that group boundaries show up in the - // copied output. - if (i > 0 && item.classList.contains("webconsole-new-group")) { - newGroup = true; - } - // Ensure the selected item hasn't been filtered by type or string. if (!item.classList.contains("hud-filtered-by-type") && !item.classList.contains("hud-filtered-by-string")) { let timestampString = l10n.timestampString(item.timestamp); - if (newGroup) { - strings.push("--"); - newGroup = false; - } - if (aOptions.linkOnly) { strings.push(item.url); } @@ -2741,6 +2738,9 @@ WebConsoleFrame.prototype = { this.jsterm.destroy(); this.jsterm = null; } + this.output.destroy(); + this.output = null; + if (this._contextMenuHandler) { this._contextMenuHandler.destroy(); this._contextMenuHandler = null; @@ -4981,17 +4981,7 @@ WebConsoleConnectionProxy.prototype = { return; } - if (aEvent == "will-navigate" && !this.owner.persistLog) { - this.owner.jsterm.clearOutput(); - } - - if (aPacket.url) { - this.owner.onLocationChange(aPacket.url, aPacket.title); - } - - if (aEvent == "navigate" && !aPacket.nativeConsoleAPI) { - this.owner.logWarningAboutReplacedAPI(); - } + this.owner.handleTabNavigated(aEvent, aPacket); }, /** diff --git a/browser/themes/shared/devtools/webconsole.inc.css b/browser/themes/shared/devtools/webconsole.inc.css index 550abfd265f3..f8f4ed86c8f2 100644 --- a/browser/themes/shared/devtools/webconsole.inc.css +++ b/browser/themes/shared/devtools/webconsole.inc.css @@ -272,3 +272,17 @@ .webconsole-msg-security.webconsole-msg-warn { -moz-image-region: rect(32px, 24px, 40px, 16px); } + +.navigation-marker { + color: #aaa; + background: linear-gradient(#fff, #bbb, #fff) no-repeat left 50%; + background-size: 100% 2px; + -moz-margin-start: 3px; + -moz-margin-end: 6px; + font-size: 0.9em; +} + +.navigation-marker .url { + background: #fff; + -moz-padding-end: 6px; +}